halo2ccs+
{"title": "halo2ccs+", "website": "https://github.com/pnyda/halo2-ccs-plus/", "createdAt": 1742981565427, "userGithub": "pnyda", "credentials": {"github": {"type": ["VerifiableCredential"], "proof": {"type": "EthereumEip712Signature2021", "created": "2025-03-26T10:27:13.287Z", "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", "proofValue": "0x275c825d0902b1dd79166115ea579db91f9ac5e0d2f67fd053cde04b7f460af665a15c9f03f86b0fca5ec7d2113349ddc902ef59fc825a7183db9c347e5c78de1c", "eip712Domain": {"types": {"Proof": [{"name": "@context", "type": "string"}, {"name": "created", "type": "string"}, {"name": "proofPurpose", "type": "string"}, {"name": "type", "type": "string"}, {"name": "verificationMethod", "type": "string"}], "@context": [{"name": "nullifiers", "type": "NullifiersContext"}, {"name": "provider", "type": "string"}], "Document": [{"name": "@context", "type": "string[]"}, {"name": "credentialSubject", "type": "CredentialSubject"}, {"name": "expirationDate", "type": "string"}, {"name": "issuanceDate", "type": "string"}, {"name": "issuer", "type": "string"}, {"name": "proof", "type": "Proof"}, {"name": "type", "type": "string[]"}], "EIP712Domain": [{"name": "name", "type": "string"}], "CredentialSubject": [{"name": "@context", "type": "@context"}, {"name": "nullifiers", "type": "string[]"}, {"name": "id", "type": "string"}, {"name": "provider", "type": "string"}], "NullifiersContext": [{"name": "@container", "type": "string"}, {"name": "@type", "type": "string"}]}, "domain": {"name": "VerifiableCredential"}, "primaryType": "Document"}, "proofPurpose": "assertionMethod", "verificationMethod": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb#controller"}, "issuer": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb", "@context": ["https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1"], "issuanceDate": "2025-03-26T10:27:13.286Z", "expirationDate": "2025-06-24T10:27:13.286Z", "credentialSubject": {"id": "did:pkh:eip155:1:0x38f92EA0f089356E8b478AaAf2802e368470235d", "@context": {"provider": "https://schema.org/Text", "nullifiers": {"@type": "https://schema.org/Text", "@container": "@list"}}, "provider": "ClearTextGithubOrg#pnyda3#135603985", "nullifiers": ["v0.0.0:0rwrp6ehrPjKfdAKdsmunCtbSPT+EcIhE5DlX/53Kjg="]}}, "twitter": {"type": ["VerifiableCredential"], "proof": {"type": "EthereumEip712Signature2021", "created": "2025-03-26T09:32:32.540Z", "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", "proofValue": "0xd88e1bc0f427a0d2503c8eed4a0c92188b6e048f02bf9f7862b249a735d8120d267472d19b82338aea6e4639727428db23794309b291c27e0fa6d10579be919c1c", "eip712Domain": {"types": {"Proof": [{"name": "@context", "type": "string"}, {"name": "created", "type": "string"}, {"name": "proofPurpose", "type": "string"}, {"name": "type", "type": "string"}, {"name": "verificationMethod", "type": "string"}], "@context": [{"name": "nullifiers", "type": "NullifiersContext"}, {"name": "provider", "type": "string"}], "Document": [{"name": "@context", "type": "string[]"}, {"name": "credentialSubject", "type": "CredentialSubject"}, {"name": "expirationDate", "type": "string"}, {"name": "issuanceDate", "type": "string"}, {"name": "issuer", "type": "string"}, {"name": "proof", "type": "Proof"}, {"name": "type", "type": "string[]"}], "EIP712Domain": [{"name": "name", "type": "string"}], "CredentialSubject": [{"name": "@context", "type": "@context"}, {"name": "nullifiers", "type": "string[]"}, {"name": "id", "type": "string"}, {"name": "provider", "type": "string"}], "NullifiersContext": [{"name": "@container", "type": "string"}, {"name": "@type", "type": "string"}]}, "domain": {"name": "VerifiableCredential"}, "primaryType": "Document"}, "proofPurpose": "assertionMethod", "verificationMethod": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb#controller"}, "issuer": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb", "@context": ["https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1"], "issuanceDate": "2025-03-26T09:32:32.540Z", "expirationDate": "2025-06-24T09:32:32.540Z", "credentialSubject": {"id": "did:pkh:eip155:1:0x38f92EA0f089356E8b478AaAf2802e368470235d", "@context": {"provider": "https://schema.org/Text", "nullifiers": {"@type": "https://schema.org/Text", "@container": "@list"}}, "provider": "ClearTextTwitter#pnyda333", "nullifiers": ["v0.0.0:aAj+RdEL8+4W5ZkX/hXG/MyAOkq2v5Et2pcWhKNCPew="]}}}, "description": "## TL;DR\n\nHappy to release [halo2 ccs+](https://github.com/pnyda/halo2-ccs-plus/), a transpiler that converts [zcash/halo2](https://github.com/zcash/halo2) circuits (with or without without lookups) into [CCS](https://eprint.iacr.org/2023/552) circuits. At the end of this post, we also go over Poseidon hash benchmarks for our transpiler and discuss our results.\n\n## Preliminaries\n\n[Plonkish](https://zcash.github.io/halo2/concepts/arithmetization.html) is a popular arithmetization used by various projects in the ecosystem, such as [ZCash](https://z.cash/), as well as [Scroll](https://scroll.io/). [Halo2](https://zcash.github.io/halo2/) is a popular proof system implementation that supports Plonkish arithmetization. It consists in 3 types of constraints: custom gates, copy constraints and lookup constraints. We point the reader to the [Halo2 book](https://zcash.github.io/halo2/) as an helpful resource to obtain more details.\n\nThe paper [*Customizable Constraint System for Succinct Arguments*](https://eprint.iacr.org/2023/552) introduced CCS, an arithmetization well-suited for folding schemes like [HyperNova](https://eprint.iacr.org/2023/573). CCS aims to generalize R1CS, Plonkish, and AIR simultaneously. While the paper defines an algorithm to convert a Plonkish circuit into a CCS circuit, there is to my knowledge no existing implementation capable of transpiling production-ready Plonkish circuits into CCS. To address this, I developed a transpiler that converts Plonkish circuits written in [Halo2](https://github.com/zcash/halo2) into CCS - using the [Sonobe](https://github.com/privacy-scaling-explorations/sonobe) implementation.\n\nIn CCS, we can define constraints equivalent to custom gates and copy constraints. However, lookup constraints do not have an equivalent representation in CCS. To address this, CCS authors introduced an arithmetization called CCS+, which combines CCS with lookup constraints. However, we found that CCS+ is not expressive enough to fully represent the lookup constraints used in the Halo2 ecosystem. We will discuss this further later.\n\n## Challenges I faced\n\n### Supporting multiple lookup tables\n\nCCS+, as defined in the original paper, supports only one lookup table. However, Halo2 allows us to define multiple lookup tables. We managed to generalize CCS+ to support multiple lookup tables.\n\n### Handling of dynamic lookups\n\nCCS+, as defined in the paper, requires the lookup table to be a public fixed vector $T$. This means that (1) the lookup table cannot be private, and (2) witnesses cannot be part of a lookup table, as the PSE fork of Halo2 allows them to be.\n\nThat is why we chose to use zcash/halo2 instead of the PSE fork of Halo2. zcash/halo2 only supports public fixed lookup tables, which simplified our work. It might be in the interest of the community to formally define a revised version of CCS+ that supports dynamic private lookup tables, enabling the transpilation of circuits developed for the PSE fork of Halo2 into CCS.\n\n### Handling of lookup inputs\n\nHalo2 lets us constrain an [`Expression`](https://docs.rs/halo2_proofs/latest/halo2_proofs/plonk/enum.Expression.html) evaluated at each row to be in a lookup table. However, CCS+, as defined in the paper, does not allow us to constrain an arbitrary expression evaluated at each row to be in a lookup table. Thus, we needed to:\n\n1. Evaluate the expression at each row in the original Plonkish table.\n2. Append the evaluation results into the witness vector $Z$.\n3. Constrain those new witnesses according to the expression, using CCS.\n4. Constrain the new witnesses to be in a lookup table, using CCS+.\n\n### Removing unconstrained witnesses\n\nIn Halo2, the height of a Plonkish table must be a power of 2. This means that when the number of rows required for the circuit does not exactly fit a power of 2, a lot of unconstrained padding cells must be introduced. For example, if you need only 129 rows, you must create a Plonkish table with a height of 256, leaving 127 cells unconstrained.\n\nTo achieve this, a custom gate is introduced that looks like `selector * constraint = 0`, where `selector` is a fixed column. The `selector` is assigned to be 1 for the rows where you want to enable the constraint (the first 129 rows in the example) and 0 for the rows where you want to disable the constraint (the last 127 rows in the example).\n\nCCS doesn't require these padding cells, so it's desirable to remove them from the witness vector $Z$[^padding].\n\nTo effectively reduce the size of the witness vector $Z$, it was necessary to detect and remove cells that are either:\n\n- Literally unconstrained, meaning there is no custom gate constraining the cell.\n- Virtually unconstrained, meaning there are custom gates constraining the cell, but all of them will evaluate to 0 regardless of the assignments to advice/instance cells, due to assignments on fixed cells.\n\nFirst, recall the CCS formula:\n\n$$\\sum_{i=0}^{q-1} c_i \\cdot \\bigcirc_{j\\in{S_i}} M_j \\cdot z = 0$$\n\nNow, we show how to reduce the sized of $Z$, in three steps:\n\n1. For some $a, b, k$, when we detect a situation where the $k$-th row of $M_a \\cdot Z$ is destined to be multiplied by 0 regardless of the assignments on $Z$, due to the existence of some $M_b$ where the $k$-th row is a zero vector, we update the $k$-th row of $M_a$ to be a zero vector. This has no effect on the overall constraint, as the $k$-th row of $M_a$ had no impact on $\\bigcirc_{j \\in S} M_j \\cdot Z$ anyway.\n\n2. For some $i$, after updating all $M$ matrices, if the $i$-th column of all $M$ matrices happens to be a zero vector, we can conclude that the $i$-th row of $Z$ is either *literally* or *virtually* unconstrained, as it was referenced by none of the $M$ matrices.\n\n3. Remove $Z[i]$ from $Z$.\n\n## Benchmarks\n\nCCS has an advantage over R1CS because it supports high-degree gates, resulting in fewer witnesses. This, in turn, leads to a smaller degree of the polynomials we need to commit to, which may contribute to faster proving times. In addition to this, CCS has an advantage over Plonkish as it supports witness deduplication. Unlike Plonkish, where we need to commit to the same witness multiple times, CCS eliminates the copy-constraint redundancy. This results in smaller witnesses, leading to smaller polynomial degrees to commit to, and potentially faster proving times[^remark5]\n\nWith this in mind, we benchmarked effective witness size reduction when transpiling a production ready halo2 circuits to CCS.\n\n### Poseidon hash\n\nWe transpiled [a Poseidon hash function implemented in Plonkish by Electric Coin Company](https://github.com/zcash/halo2/tree/main/halo2_poseidon) into CCS. Additionally, we compared the number of witnesses in the transpiled CCS circuit with [a Poseidon hash function implemented in R1CS by the circomlib team](https://zkrepl.dev/?gist=5cef0dde9ea4603810c9154394371289). Our benchmark code can be found [here](https://github.com/pnyda/halo2-ccs-plus/blob/master/tests/poseidon.rs). \n\n|             | CCS | Plonkish     | R1CS         |\n|-------------|-----|--------------|--------------|\n| Witnesses   | 142 | 320 (~2.25x) | 243 (~1.71x) |\n| Constraints | 139 | 652[^num_constraints] (~4.69x)         | 240[^number_of_rounds] (~1.72x) |\n\n### Witness size reduction vs Plonkish\n\n- Plonkish advice/instance cells: 320\n- CCS witness length: 142\n- Reduction ratio: ~0.44x\n\nThis reduction is largely due to the removal of unconstrained witnesses, as described above. The Poseidon hash implementation by Electric Coin Company only uses 12 copy constraints, so only 12 witnesses were removed due to these constraints.\n\n### Witness size reduction vs R1CS\n\n- R1CS witness length: 243\n- CCS witness length: 142\n- Reduction ratio: ~0.58x\n\nThis reduction is largely due to high-degree gates. The transpiled circuit has a degree of 5, compared to the circomlib implementation, which has a degree of 2.\n\n### Constraints reduction vs Plonkish\n\n- The number of custom gates: 10\n- The number of rows in the Plonkish table: $2^6$ = 64\n- The number of copy constraints: 12\n- The number of Plonkish constraints: 10 * 64 + 12 = 652[^num_constraints]\n- $M$ matrices height in CCS: 139\n- Reduction ratio: ~0.21x\n\nThis reduction is largely due to removal of disabled custom gates. In Plonkish arithmetization, there is no way for us to selectively apply a custom gate only on some rows. Rather, all custom gates are applied on all rows. We often workaround this by introducing selector columns, as described in previous section. This contrasts with CCS where we can selectively apply a constraint only on specific witnesses. Our transpiler exploits this fact, and generates minimal $M$ matrices, in a sense that they don't include the constraints generated from a custom gate applied at a row, if the gate's selector was turned off at the row. This leads to shorter height of $M$ matrices, and smaller number of constraints.\n\n### Constraints reduction vs R1CS\n\n- $A$, $B$, $C$ matrices height in R1CS: 240\n- $M$ matrices height in CCS: 139\n- Reduction ratio: ~0.58x\n\nThis reduction is largely due to high-degree gates. The transpiled circuit has a degree of 5, whereas the circomlib implementation has degree 2.\n\n## Conclusion\n\nAlthough we believe there is plenty of room for optimization in the codebase, transpiling today runs in 4 seconds on an Intel Core i7 1260P.. Contributions are welcome!\n\nThanks to @Pierre for reviewing this post as well as the codebase and to @CPerezz for advising on Halo2 internals.  This work was done in the context of a grant with [PSE](https://pse.dev/).\n\n[^padding]: While CCS itself doesn't need padding cells, proof systems that use CCS might still require the witness vector $Z$ to be a power of 2. Since this transpiler is agnostic to proof systems, this concern is not addressed in this implementation.\n\n[^remark5]: See remark 5 in the CCS paper.\n\n[^num_constraints]: Since Plonkish arithmetization has multiple types of constraints, we can't compare this number directly to the height of $A$, $B$, $C$ in R1CS.\n\n[^number_of_rounds]: The parameters used in circomlib's Poseidon implementation and the parameters used in halo2_gadgets' Poseidon implementation slightly differ.", "projectGithub": "pnyda3", "projectTwitter": "pnyda333"}