diff --git a/README.md b/README.md index 8d1ecae..6776668 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ Simplicity has no pairing jets, so we applied a bunch of tricks to fit Groth16 v | | | |---|---| -| program cost, one public signal | 81.7 M mWU | -| transaction | 107.4 kWU, 26.9 kvB | -| pruned program / witness | 61 kB / 45 kB | -| fee at 0.115 sat/vB | ~ 3,100 sat | +| program cost, one public signal | 68.5 M mWU | +| transaction | 90.8 kWU, 22.7 kvB | +| pruned program / witness | 60 kB / 30 kB | +| fee at 0.115 sat/vB | ~ 2,600 sat | The program's bytes earn more budget than it costs, so it needs no padding. The transaction is about a quarter of the 400 kWU standard limit. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 393e6e3..b7c28d8 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -4,30 +4,43 @@ All costs are Simplicity's static cost bound in milliweight units (mWU), as rust ## One spend -The toy circuit's verifier (one public signal bound to the transaction), spent on an Elements 23.3.1 regtest node in `9f4c1cb0…`: +The toy circuit's verifier (one public signal bound to the transaction), spent on an Elements 23.3.1 regtest node in `511a3da3…`: | | | |---|---| -| program cost | 81,743,632 mWU | -| budget: (witness stack bytes + 50) x 1000 | 106,812,000 mWU, no padding needed | -| pruned program | 61,411 bytes | -| witness (proof, Miller values, slopes, quotient, Merkle sibling) | 45,281 bytes | -| transaction | 107,423 WU, 26,856 vB | -| fee at 0.115 sat/vB, which Liquid testnet accepts | about 3,100 sat | +| program cost | 68,491,902 mWU | +| budget: (witness stack bytes + 50) x 1000 | 90,195,000 mWU, no padding needed | +| pruned program | 60,346 bytes | +| witness (proof, 35 Miller values, slopes, quotient, Merkle sibling) | 29,729 bytes | +| transaction | 90,804 WU, 22,701 vB | +| fee at 0.115 sat/vB, which Liquid testnet accepts | about 2,600 sat | ## Where the cost goes | part | mWU | share | |---|---:|---:| -| the loop: 88 steps, each a few relations at z | 66,612,776 | 81.5% | -| MSM of the public input: 51 windows of 5 bits | 8,645,785 | 10.6% | -| setup: on-curve checks, powers of z, c(z), the Frobenius images, the transcript header | 3,166,661 | 3.9% | -| reading the witness, passing it between the parts | 2,144,910 | 2.6% | -| end: the last relations, Q(z), the two hashes | 1,147,204 | 1.4% | +| the loop: 88 steps, each a few relations at z | 51,949,542 | 75.8% | +| MSM of the public input: 51 windows of 5 bits | 8,645,785 | 12.6% | +| end: the last relations, Q(z) (200 coefficients), the two hashes | 3,305,204 | 4.8% | +| setup: on-curve checks, powers of z, c(z), the Frobenius images, the transcript header | 3,166,661 | 4.6% | +| reading the witness, passing it between the parts | 1,398,414 | 2.0% | | transaction binding, range check | 26,296 | 0.0% | -| total | 81,743,632 | | +| total | 68,491,902 | | -Of the loop, 55.6 M is in the steps and 11.0 M in folding the loop state over the witness: the state carries 33 per-run values (the powers of z, the points at z, r) to every step. +Of the loop, 43.8 M is in the steps and 8.2 M in folding the loop state over the witness: the state carries 33 per-run values (the powers of z, the points at z, r) to every step. + +## Where the bytes go + +| | bytes | +|---|---:| +| line constants of γ and δ: 4 words per pair and step | 22,528 | +| program code | about 26,000 | +| MSM: selected table entries and pruned branches | about 11,400 | +| 35 committed Miller values | 13,440 | +| quotient Q, 200 coefficients | 6,400 | +| B's slopes, 88 steps | 5,632 | +| inverses proving xQ != x, 23 additions | 1,472 | +| MSM slopes, c, c^-1, the proof, the challenges | about 2,900 | ## Building blocks @@ -38,22 +51,23 @@ Of the loop, 55.6 M is in the steps and 11.0 M in folding the loop state over th | F_p product with a constant, or normalisation | 8,923 | | F_p addition | 2,579 | | 12-term dot product, one reduction | 116,744 | -| loop step: doubling (65 of them) | 609,273 | -| loop step: addition with c or c^-1 (21) | 698,901 | -| loop step: Frobenius addition (2) | 683,092 | +| loop step: doubling (51 of them) | 385,831 | +| loop step: doubling with a committed Miller value (14) | 608,873 | +| loop step: addition with c or c^-1, committed (21) | 698,901 | +| loop step: Frobenius addition (2) | 455,950 | +| Q(z): eight coefficients (25 calls) | 94,597 | | MSM window (select and add) | about 169,500 | -A doubling step evaluates the committed Miller value at z (a 12-term dot product), B's slope, the three lines, and adds its two relations to the Horner sum. Computed in the tower, the same step cost 3,644,556. +A doubling step evaluates B's slope and the three lines at z and adds its relations to the Horner sum; a committed one also evaluates the committed Miller value (a 12-term dot product). Computed in the tower, the same step cost 3,644,556. ## Before and after -| | tower Miller loop | one polynomial identity | +| | tower Miller loop (main) | one polynomial identity | |---|---:|---:| -| program cost | 371.8 M mWU (with 414 padding blocks) | 81.7 M mWU | -| pruned program | 161 kB | 61 kB | -| witness | 220 kB (212 kB of it padding) | 45 kB | -| transaction | 382.2 kWU, 95.6 kvB | 107.4 kWU, 26.9 kvB | -| room under the 400 kWU limit | one more public input | about 20 more | +| program cost | 371.8 M mWU (with 414 padding blocks) | 68.5 M mWU | +| pruned program | 161 kB | 60 kB | +| witness | 220 kB (212 kB of it padding) | 30 kB | +| transaction | 382.2 kWU, 95.6 kvB | 90.8 kWU, 22.7 kvB | ## Time @@ -61,7 +75,7 @@ A doubling step evaluates the committed Miller value at z (a 12-term dot product |---|---| | generate the program from a key (Python) | 2 s | | generate the witness: Miller values, residue witness, quotient (Python) | 2 s | -| `deploy compile`: parse the 1.1 MB program text, once per key | 2.6 min | +| `deploy compile`: parse the 1.1 MB program text, once per key | 2.5 min | | `deploy spend`: prune, execute, build the transaction | 1 s | | snarkjs setup / proof of the toy circuit | 1.3 min / 1 s | | `tests/test_groth16.py` | 3 min | @@ -82,7 +96,7 @@ python3 tests/test_groth16.py # the whole verifier python3 tests/test_binding.py # the verifier with the transaction binding ``` -`deploy spend` prints the cost, the sizes and the budget of a real transaction, and `tools/harness/target/release/costs ` the cost of every named definition of a program. The files in `examples/multiplier/` regenerate the program and the witness of the regtest spend `9f4c1cb0…` byte for byte: +`deploy spend` prints the cost, the sizes and the budget of a real transaction, and `tools/harness/target/release/costs ` the cost of every named definition of a program. The files in `examples/multiplier/` regenerate the program and the witness of the regtest spend `511a3da3…` byte for byte: ```sh E=examples/multiplier @@ -95,8 +109,8 @@ gunzip -c $E/verifier.simpl.gz | cmp - build/check.simpl && gunzip -c $E/verifie | network | transaction | what | |---|---|---| -| Elements 23.3.1 regtest | `0da39ffd…` | the walkthrough (`tools/regtest.sh`): proof spend, 107,421 WU, vsize 26,856 | -| Elements 23.3.1 regtest | `901f2f6a…` | the same kind of proof redirected to another address: rejected with `Assertion failed inside jet` | -| Elements 23.3.1 regtest | `9f4c1cb0…` | proof spend, the files in `examples/multiplier/` | -| Elements 23.3.1 regtest | `d8d87a6a…` | the key path of the same leaf, 721 WU | +| Elements 23.3.1 regtest | `beba377d…` | the walkthrough (`tools/regtest.sh`): proof spend, 90,804 WU, vsize 22,701 | +| Elements 23.3.1 regtest | `511a3da3…` | proof spend, the files in `examples/multiplier/` | +| Elements 23.3.1 regtest | a redirected proof | rejected with `Assertion failed inside jet` | +| Elements 23.3.1 regtest | `5b6b46f1…` | the key path of the same leaf, 721 WU | | Liquid testnet | [`f25a6e90…`](https://blockstream.info/liquidtestnet/tx/f25a6e90417c427f25c1d6883869825f30ec111a357b0e550bf24a20725d3cdb), block 2,659,534 | an earlier verifier (tower Miller loop, without the G2 check): 381,492 WU, fee 11,000 sat | diff --git a/docs/design.md b/docs/design.md index 914bdcf..e644e7d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -117,12 +117,14 @@ The Groth16 equation e(A, B) = e(α, β) e(L, γ) e(C, δ) holds if and only if The Miller loop runs along the signed-binary (NAF) digits of 6x + 2, the loop length of BN254's optimal ate pairing, and ends with two additions by Frobenius images of each G2 point: 88 steps, 65 doublings and 23 additions. The three pairs (-A, B), (L, γ) and (C, δ) advance in lockstep, so the accumulator f is squared once per step for all of them. The fourth pair, (α, β), belongs to the verifying key; its Miller value is a constant of the program. -Each step multiplies f (squared, for a doubling) by three lines, one per pair, each of the form `yP + c3 xP w + c4 w^3` with c3 = -λ and c4 = λ xT - yT, where T is the pair's running G2 point and λ the slope of the step. The prover writes the new value f' into the witness, and the step becomes a relation between polynomials: +Each step multiplies f (squared, for a doubling) by three lines, one per pair, each of the form `yP + c3 xP w + c4 w^3` with c3 = -λ and c4 = λ xT - yT, where T is the pair's running G2 point and λ the slope of the step. When the prover writes the new value f' into the witness, the step becomes a relation between polynomials: ``` f^2 l_B l_γ l_δ - f' = 0 (mod M) ``` +The prover does not commit after every step. Without a commitment, f' is the product itself, a polynomial of higher degree that the next relation carries along, and that next relation checks two steps at once. A committed value costs 12 witness words; a higher degree costs words of the quotient (below). The generator skips a commitment whenever the identity's degree stays within 211, the cap with the fewest words in total: 35 of the 88 Miller values are committed. + γ and δ belong to the key, so their slopes and lines are computed when the program is generated: each step receives c3 and c4 for both, four words each. B comes from the proof. The prover writes only B's slopes; B's points are never written down. The new point of a doubling is x' = λ^2 - 2x, y' = λ (x - x') - y, a polynomial in the slopes, and the program checks each slope instead: ``` @@ -148,7 +150,7 @@ The loop also proves that B is in G2. Its last point is T = [6x+2]B + ψ(B) - ψ ### One identity, checked at one point -That makes 203 relations R_k, each a polynomial in w of degree at most 61. The prover combines them with the powers of a challenge r into S = Σ r^k R_k, divides S by M, and writes the quotient Q (50 coefficients) into the witness. The program then picks a point z, evaluates every polynomial at z, and checks +That makes 150 relations R_k, each a polynomial in w of degree at most 211. The prover combines them with the powers of a challenge r into S = Σ r^k R_k, divides S by M, and writes the quotient Q (200 coefficients) into the witness. The program then picks a point z, evaluates every polynomial at z, and checks ``` S(z) = Q(z) M(z) @@ -156,13 +158,13 @@ S(z) = Q(z) M(z) This is randomized extension-field arithmetic: [Garaga](https://hackmd.io/@feltroidprime/B1eyHHXNT) checks each F_p12 product with its own quotient, and Novakovic and Eagen (section 6) combine all relations into one quotient, as here. Two things are specific to this verifier: the challenges come from SHA-256 inside the program, and B's F_p2 arithmetic is part of the same identity, so B's points never enter the witness. -The program never multiplies in F_p12. A committed value f' becomes the number f'(z), a 12-term dot product with the powers of z; a line becomes a number; a step's relation becomes a few F_p products, added to a running Horner sum of S(z). A doubling step costs 0.61 M mWU, against 3.6 M for computing it in the tower. +The program never multiplies in F_p12. A committed value f' becomes the number f'(z), a 12-term dot product with the powers of z; a line becomes a number; a step's relation becomes a few F_p products, added to a running Horner sum of S(z). A doubling step costs 0.39 M mWU, or 0.61 M with a commitment, against 3.6 M for computing it in the tower. Q(z) is evaluated eight coefficients at a time by one shared function, so its code is in the program once. -r and z come from SHA-256 (Fiat-Shamir): r hashes the public inputs, the proof, c, c^(-1), s and everything the loop committed, and z hashes r and Q. The program gets r and z as witness words, uses them throughout, and at the end compares them with the two hashes it has computed along the way. If some relation does not hold, S is nonzero modulo M except for at most 203 values of r, and then S - Q M is a nonzero polynomial of degree at most 61, which vanishes at z with probability at most 61/p. A cheating prover must find a hash output in a set of relative size about 2^-246. +r and z come from SHA-256 (Fiat-Shamir): r hashes the public inputs, the proof, c, c^(-1), s and everything the loop committed, and z hashes r and Q. The program gets r and z as witness words, uses them throughout, and at the end compares them with the two hashes it has computed along the way. If some relation does not hold, S is nonzero modulo M except for at most 150 values of r, and then S - Q M is a nonzero polynomial of degree at most 211, which vanishes at z with probability at most 211/p. A cheating prover must find a hash output in a set of relative size about 2^-245. The committed words are checked to be below 2^254 (their two top bits are zero), so that the 12-term dot products fit the bound of the field arithmetic; every canonical word passes. The program normalises the challenges, whose words are arbitrary 256-bit hashes. -The witness grows to 45 kB, mostly the 88 committed Miller values, and the program shrinks to 61 kB, since the line data of γ and δ is now four words per pair and step instead of eighteen. +The witness is 30 kB: the committed Miller values (13 kB), the quotient (6 kB), B's slopes (6 kB) and the rest of the proof. The program is 60 kB; the line data of γ and δ, four words per pair and step, is 22 kB of it. ## Public input and transaction binding (curve.py, merkle.py, groth16.py) @@ -172,7 +174,7 @@ To need only one, a circuit for this verifier exposes a single public signal: th ## Budget and padding (groth16.py) -The program costs 81.7 M mWU. Its budget is 1,000 mWU per byte of the input's witness stack, so the 61 kB program and the 45 kB witness already earn about 107 M mWU, and no padding is needed: the input weighs about 107 kWU. +The program costs 68.5 M mWU. Its budget is 1,000 mWU per byte of the input's witness stack, so the 60 kB program and the 30 kB witness already earn about 90 M mWU, and no padding is needed: the transaction weighs about 91 kWU. Programs that cost more than their bytes earn can still be padded. Elements does not relay transactions with an annex, the field rust-simplicity would use for padding, so the blocks of 512 zero bytes go into the Simplicity witness. A witness value the program never reads would get the wrong type, so each block is hashed into a throwaway SHA-256 context, which costs 25.5 k mWU per block. `Groth16.padding` sizes the padding from the program's cost, and `deploy spend` reports whether the budget is covered. diff --git a/examples/README.md b/examples/README.md index 4979fe5..726a3b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,13 +16,13 @@ tools/harness/target/release/hand examples/fp_mul.simpl examples/fp_mul.w0.json ## multiplier/ -The verifier of `circuits/multiplier.circom` (a·b = c, bound to the spending transaction) and the files of one spend on an Elements regtest node, transaction `9f4c1cb0…`, which proves c = 21. +The verifier of `circuits/multiplier.circom` (a·b = c, bound to the spending transaction) and the files of one spend on an Elements regtest node, transaction `511a3da3…`, which proves c = 21. | file | | |---|---| | `verification_key.json` | the snarkjs key | | `verifier.simpl.gz` | the generated program, 1.1 MB of text | -| `verifier.uprog` | its consensus encoding before pruning (`deploy compile`), CMR `c910831b…26b0be62` | +| `verifier.uprog` | its consensus encoding before pruning (`deploy compile`), CMR `e502d43b…47fb0d6b` | | `proof.json`, `public.json` | the snarkjs proof and its public signal | | `leaves.json` | the two leaves behind the public signal: the transaction's sighash and c | | `verifier.w0.json.gz` | the witness: proof, Miller values, slopes, quotient, Merkle sibling | diff --git a/examples/multiplier/leaves.json b/examples/multiplier/leaves.json index e7860cf..09e32d2 100644 --- a/examples/multiplier/leaves.json +++ b/examples/multiplier/leaves.json @@ -1 +1 @@ -{"sighash": "2430abe796bfee9e30559fe742228b1d36004de4a4b0c4cb945366aa31bb7321", "values": ["21"]} \ No newline at end of file +{"sighash": "cdac35b192915c6ebbc240dd8c4f8d22cf4e5e13087fb23e9978fbc6ce2cb05c", "values": ["21"]} \ No newline at end of file diff --git a/examples/multiplier/proof.json b/examples/multiplier/proof.json index daa673f..74faff6 100644 --- a/examples/multiplier/proof.json +++ b/examples/multiplier/proof.json @@ -1,17 +1,17 @@ { "pi_a": [ - "7614975239070624198332854401873649505470572626547896149741070312897843358930", - "4611774705360608746977331987834566924868049144042721246704404533437671643615", + "21566684453736135585297791733111880454710809102449812304555056051874966362175", + "20439253185853980828249216164238736927967952351267181741770936990431369438778", "1" ], "pi_b": [ [ - "5261944696180792681624872483670754604546279463032739224986573158204266522465", - "12317633613943012002930990405081707670179665685030284658975824394534056369377" + "2376322599524250606318122939551750522956957733513865503231378596442919457301", + "21315688201840938592689832640776347449862151534123330070129388622190619729110" ], [ - "21154522702036096038089906596923283702887853601323724616219725041146024442675", - "17356759660496773857362515270681502939595449805612756466401971378462211378698" + "3943731764001132601666642157228626600487124330574975950047211703846244120826", + "9258774679406286764135352324685156081411875551338919970350745361914487123535" ], [ "1", @@ -19,8 +19,8 @@ ] ], "pi_c": [ - "3199714410403655289905644902552955024418647404663178093197136563332313053728", - "11760199718826279720608869179394965276413967892246556778442807843086576418841", + "4638708091212589675127653121818387886945714069030086334439062558372181182618", + "8670616252775378861952872650202232831163409290031897847958971087012591991932", "1" ], "protocol": "groth16", diff --git a/examples/multiplier/public.json b/examples/multiplier/public.json index fb4754e..15dd29d 100644 --- a/examples/multiplier/public.json +++ b/examples/multiplier/public.json @@ -1,3 +1,3 @@ [ - "11319441843641788037619681105067091827983231747872427158058865524502604764122" + "51333353947613502813995737443488039187621314919488324933339036886237847445" ] \ No newline at end of file diff --git a/examples/multiplier/verifier.simpl.gz b/examples/multiplier/verifier.simpl.gz index 484105a..92bd14e 100644 Binary files a/examples/multiplier/verifier.simpl.gz and b/examples/multiplier/verifier.simpl.gz differ diff --git a/examples/multiplier/verifier.uprog b/examples/multiplier/verifier.uprog index 24d5966..5f36409 100644 Binary files a/examples/multiplier/verifier.uprog and b/examples/multiplier/verifier.uprog differ diff --git a/examples/multiplier/verifier.w0.json.gz b/examples/multiplier/verifier.w0.json.gz index e8fa016..2b3e871 100644 Binary files a/examples/multiplier/verifier.w0.json.gz and b/examples/multiplier/verifier.w0.json.gz differ diff --git a/groth/pairing.py b/groth/pairing.py index 5103f2d..8066833 100644 --- a/groth/pairing.py +++ b/groth/pairing.py @@ -2,26 +2,30 @@ F_p12 is F_p[w] / M(w), M = w^12 - 18 w^6 + 82 (poly.py), so every product the check needs, in F_p12 and in the F_p2 arithmetic of B's points, is a polynomial product modulo M. The prover writes the Miller -value after every step into the witness, and each step becomes a few relations R(w) = 0 (mod M): +value after some of the steps (COMMIT) into the witness, and each step becomes a few relations +R(w) = 0 (mod M): doubling f^2 l_B l_gamma l_delta - f', lambda y - 3/2 x^2 addition f g l_B l_gamma l_delta - f', lambda (xQ - x) - (yQ - y), (xQ - x) d - 1 at the end x - x(-psi^3(B)), y - y(-psi^3(B)), f pi(c^-1) pi^2(c) pi^3(c^-1) - T_s, c c^-1 - 1 -f, f' are the committed Miller values, g = c^-1 or c (the residue witness, as in Novakovic and Eagen), +f, f' are the Miller values, g = c^-1 or c (the residue witness, as in Novakovic and Eagen), l the lines at P = -A, L, C. Only B's slopes lambda are committed: x' = lambda^2 - 2x and y' = lambda (x - x') - y are polynomials in them. A slope that satisfies its relation is the true one (y is never 0, and d, the inverse of xQ - x, proves xQ != x), so every step is the group law. The last three relations are the G2 membership of B and the residue check; T_s = (W27^s e(alpha, beta))^-1. +After a step whose value is not committed, f' is the product itself, so the next relation carries a +polynomial of higher degree; the first relation of a step is dropped then. The prover combines the relations with powers of a challenge r, S = sum r^k R_k, and supplies the quotient Q = S / M. The program evaluates everything at one point z and checks S(z) = Q(z) M(z). It multiplies only in F_p: an F_p12 product becomes one product of two evaluations. Soundness. r is the SHA-256 of the public inputs, the proof and everything committed, and z the -SHA-256 of r and Q. If some R_k is not 0 modulo M, S is not 0 modulo M except for at most 203 values of -r; then S - Q M is a nonzero polynomial of degree at most 61, which vanishes at z with probability at most -61 / p. The program gets r and z as witness words and compares them with the two hashes at the end. +SHA-256 of r and Q. If some R_k is not 0 modulo M, S is not 0 modulo M except for at most 150 values of +r; then S - Q M is a nonzero polynomial of degree at most 211, which vanishes at z with probability at +most 211 / p. The program gets r and z as witness words and compares them with the two hashes at the +end. `relations` is written once over an abstract ring: the prover runs it on polynomials to get S and Q, the tests run it on numbers to compare with the program. @@ -111,9 +115,12 @@ class DegRing: elem = staticmethod(lambda coeffs: 11) -def relations(R, v, lines, table): +def relations(R, v, lines, table, commit=None): """The relations R_k in order, over the ring R. v: the values (Prover.witness); lines: fixed_lines of - gamma and delta; table: T_s for s = 0, 1, 2 (direct coefficients).""" + gamma and delta; table: T_s for s = 0, 1, 2 (direct coefficients); commit: the steps whose Miller + value is committed (COMMIT). After any other step, f' is the product itself, a polynomial of + higher degree.""" + commit = COMMIT if commit is None else commit X = R.X X3 = R.mul(R.mul(X, X), X) U = R.sub(R.mul(X3, X3), R.k(9)) # u = w^6 - 9 @@ -147,8 +154,10 @@ def line(Pt, c3, c4): # yP + c3 xP w + c4 w^3: the tower line c0 + c3 w + c4 v w lB = line(v['P'], R.sub(R.k(0), lam), m) lg = line(v['L'], phi(lines[0][i][0]), phi(lines[0][i][1])) ld = line(v['C'], phi(lines[1][i][0]), phi(lines[1][i][1])) - f_next = R.elem(v['f'][i]) - rels.append(R.sub(R.mul(R.mul(R.mul(f2, lB), lg), ld), f_next)) + f_next = R.mul(R.mul(R.mul(f2, lB), lg), ld) + if commit[i]: + rels.append(R.sub(f_next, R.elem(v['f'][i]))) + f_next = R.elem(v['f'][i]) x, y, f = x2, R.sub(m, R.mul(lam, x2)), f_next rels.append(R.sub(x, phi(xb.conj() * bn254.FROB3[2]))) rels.append(R.sub(y, phi(yb.conj() * -bn254.FROB3[3]))) @@ -174,16 +183,29 @@ def horner(R, rels, r): return acc -def _shape(): +def _shape(commit): """The number of relations and the degree of S (DegRing reads only the structure).""" z = bn254.Fp2(0) v = dict(P=(0, 0), B=bn254.G2_GEN, C=(0, 0), L=(0, 0), c=[0] * 12, ci=[0] * 12, s=0, lam=[z] * N_STEPS, f=[[0] * 12] * N_STEPS, d={i: z for i in range(N_STEPS)}) - rels = relations(DegRing, v, [[(z, z)] * N_STEPS] * 2, [[0] * 12] * 3) + rels = relations(DegRing, v, [[(z, z)] * N_STEPS] * 2, [[0] * 12] * 3, commit) return len(rels), max(rels) -N_RELS, DEG_S = _shape() # 203 relations, degree 61 +def schedule(cap): + """The steps whose Miller value is committed: left to right, a step is skipped whenever S stays + within the degree cap. A committed value costs 12 words of witness, a degree of S one word of Q.""" + commit = [True] * N_STEPS + for i in range(N_STEPS): + commit[i] = False + if _shape(commit)[1] > cap: + commit[i] = True + return commit + + +DEG_CAP = 211 # the cap with the fewest committed words: 36 Miller values, Q of 200 words (vs 88 and 50) +COMMIT = schedule(DEG_CAP) +N_RELS, DEG_S = _shape(COMMIT) # 150 relations, degree 211 N_Q = DEG_S - 11 # coefficients of Q @@ -209,9 +231,9 @@ def blocks(n): return out -def leaf_words(kind): - """lambda (2 words), f' (12), and d (2) for an addition.""" - return 16 if kind in ADDS else 14 +def leaf_words(kind, commit): + """lambda (2 words), f' (12) if committed, d (2) for an addition.""" + return 2 + 12 * commit + 2 * (kind in ADDS) class Prover: @@ -244,7 +266,7 @@ def witness(self, pairs, inputs, residue=None, check=True): T, _ = bn254.affine_step(T, lams[i], pts[kind]) for k in range(3): f = f.mul_by_034(*lines[k][i]) - fs.append(poly.from_tower(f)) + fs.append(poly.from_tower(f) if COMMIT[i] else None) v = dict(P=nA, B=B, C=C, L=L, c=poly.from_tower(c), ci=poly.from_tower(ci), s=s, lam=lams, f=fs, d=ds) return self.seal(v, inputs, check) @@ -255,7 +277,8 @@ def seal(self, v, inputs, check=True): fp2 = lambda a: mont([a.c0, a.c1]) nA, B, C = v['P'], v['B'], v['C'] w = dict(points=mont([nA[0], nA[1], B[0].c0, B[0].c1, B[1].c0, B[1].c1, C[0], C[1]]), c=mont(v['c']), ci=mont(v['ci']), s=v['s'], - leaves=[fp2(v['lam'][i]) + mont(v['f'][i]) + (fp2(v['d'][i]) if i in v['d'] else []) for i in range(N_STEPS)]) + leaves=[fp2(v['lam'][i]) + (mont(v['f'][i]) if COMMIT[i] else []) + (fp2(v['d'][i]) if i in v['d'] else []) + for i in range(N_STEPS)]) t = wbytes(inputs) + wbytes(w['points']) + s_byte(v['s']) + wbytes(w['c'] + w['ci']) + b''.join(map(wbytes, w['leaves'])) w['r'] = int.from_bytes(hashlib.sha256(t).digest(), 'big') q, rem = poly.divmod_m(horner(PolyRing, relations(PolyRing, v, self.lines, self.table), from_mont(w['r']))) @@ -295,11 +318,15 @@ def __init__(self, fp, gamma, delta, const, b=None): self.sample_bounds() be = self.be st = self.state_shape() - self.STEP_D = be.function('STEP_D', [[st, leaf_shape('D')], words(8)], self.step_body('D')) - self.STEP_A = be.function('STEP_A', [[st, leaf_shape('A+')], [words(3), words(8)]], self.step_body('A')) - self.STEP_F = be.function('STEP_F', [[st, leaf_shape('F1')], [words(2), words(8)]], self.step_body('F')) + self.steps = {} + for kind, args in (('D', None), ('A', words(3)), ('F', words(2))): + for commit in sorted({COMMIT[k] for k in range(N_STEPS) if STEPS[k][0] == kind}): + shape = [[st, leaf_shape({'D': 'D', 'A': 'A+', 'F': 'F1'}[kind], commit)], words(8) if args is None else [args, words(8)]] + name = f'STEP_{kind}' + ('' if commit else '_') + self.steps[(kind, commit)] = be.function(name, shape, self.step_body(kind, commit)) + self.QCHUNK = be.function('QCHUNK', [[words(2), words(7)], words(8)], self.qchunk_body) if is_sym(be): - self.costs = {fn.t: fn.c for fn in (self.STEP_D, self.STEP_A, self.STEP_F)} + self.costs = {fn.t: fn.c for fn in list(self.steps.values()) + [self.QCHUNK]} def one(self): return self.fp.const(1) def m1(self): return self.fp.const(P - 1) @@ -345,9 +372,10 @@ def sha_final(self, h): def state_shape(self): return [words(4), E('', SHA_CTX_W, 0), words(len(RUN))] - def step_body(self, kind): - """Body of STEP_D, STEP_A or STEP_F: ((state, leaf), args) -> state. Evaluates lambda, f' and - the lines at z, adds the step's relations to acc and absorbs the leaf into h.""" + def step_body(self, kind, commit): + """Body of a step function: ((state, leaf), args) -> state. Evaluates lambda and the lines at z, + and f' (a committed value at z, or the product), and adds the step's relations to acc in the + order of `relations`; absorbs the leaf into h.""" fp, be, b = self.fp, self.be, self.b rd, one, m1 = self.rd, self.one, self.m1 add = kind != 'D' @@ -355,30 +383,33 @@ def step_body(self, kind): def body(accs): (st, leaf), args = accs (e, acc, x, y), h, run = st - fw = flat(leaf[1]) + fw = flat(leaf[1]) if commit else [] data = args if kind == 'D' else args[1] run = dict(zip(RUN, run)) c = {'e': Val(e, b['e']), 'acc': Val(acc, b['acc']), 'x': Val(x, b['x']), 'y': Val(y, b['y']), - 'l': [Val(w, WORD_MAX) for w in leaf[0]], 'f': [Val(w, F_HI) for w in fw], - 'dg': [Val(w, P - 1) for w in data[:4]], 'dd': [Val(w, P - 1) for w in data[4:]], - 'zp': [Val(run[f'z{k}'], KB) for k in range(1, 12)]} - for nm in ('z3', 'zeta', 'z3zeta', 'nxpz', 'yp', 'xlz', 'xlzz', 'yl', 'xcz', 'xczz', 'yc', 'r', 'r2') + (('r3',) if add else ()): + 'l': [Val(w, WORD_MAX) for w in leaf[0]], + 'dg': [Val(w, P - 1) for w in data[:4]], 'dd': [Val(w, P - 1) for w in data[4:]]} + if commit: + c['f'] = [Val(w, F_HI) for w in fw] + c['zp'] = [Val(run[f'z{k}'], KB) for k in range(1, 12)] + for nm in ('z3', 'zeta', 'z3zeta', 'nxpz', 'yp', 'xlz', 'xlzz', 'yl', 'xcz', 'xczz', 'yc', 'r') + (('r2',) if add or commit else ()) + (('r3',) if add and commit else ()): c[nm] = Val(run[nm], KB) if add: c['xq'], c['yq'] = Val(args[0][0], KB), Val(args[0][1], KB) - c['d'] = [Val(w, WORD_MAX) for w in leaf[2]] + c['d'] = [Val(w, WORD_MAX) for w in leaf[-1]] if kind == 'A': c['g'] = Val(args[0][2], KB) at_zeta = lambda c, a: rd([(a[1], c['zeta']), (a[0], one())]) # a0 + a1 zeta def first(c): - f = c['f'] - fz = rd([(f[0], one())] + [(f[k], c['zp'][k - 1]) for k in range(1, 12)]) - out = [at_zeta(c, c['l']), fp.normalize(fz)] + out = [at_zeta(c, c['l'])] + if commit: + f = c['f'] + out.append(fp.normalize(rd([(f[0], one())] + [(f[k], c['zp'][k - 1]) for k in range(1, 12)]))) return out + ([at_zeta(c, c['d']), fp.sub(c['xq'], c['x'])] if add else []) minus_x3 = (lambda c: [(c['x'], m1()), (c['xq'], m1())]) if add else (lambda c: [(c['x'], fp.const(P - 2))]) - steps = [('let', ['lam', 'e2'] + (['dz', 'dx'] if add else []), first), - ('drop', ['l', 'f', 'zp'] + (['d'] if add else [])), + steps = [('let', ['lam'] + (['e2'] if commit else []) + (['dz', 'dx'] if add else []), first), + ('drop', ['l'] + (['f', 'zp'] if commit else []) + (['d'] if add else [])), ('let', ['x2', 'm'], lambda c: [rd([(c['lam'], c['lam'])] + minus_x3(c)), rd([(c['lam'], c['x']), (c['y'], m1())])])] def final(c, _): @@ -389,39 +420,59 @@ def final(c, _): ld = rd([(c['yc'], one()), (c['xcz'], c['dd'][0]), (c['xczz'], c['dd'][1]), (c['z3'], c['dd'][2]), (c['z3zeta'], c['dd'][3])]) if kind == 'D': f2 = rd([(c['e'], c['e'])]) - terms = [(c['acc'], c['r2']), (rd([(lam, c['y']), (c['x'], fp.mul_const(c['x'], -THREE_HALVES))]), c['r'])] + rels = [rd([(lam, c['y']), (c['x'], fp.mul_const(c['x'], -THREE_HALVES))])] else: f2 = rd([(c['e'], c['g'])]) if kind == 'A' else c['e'] - slope = rd([(lam, c['dx']), (c['yq'], m1()), (c['y'], one())]) - inverse = rd([(c['dx'], c['dz']), (one(), m1())]) - terms = [(c['acc'], c['r3']), (slope, c['r2']), (inverse, c['r'])] - acc2 = rd(terms + [(rd([(rd([(f2, lB)]), lg)]), ld), (c['e2'], m1())]) - outs = [c['e2'], acc2, x2, y2] + rels = [rd([(lam, c['dx']), (c['yq'], m1()), (c['y'], one())]), rd([(c['dx'], c['dz']), (one(), m1())])] + prod = rd([(rd([(f2, lB)]), lg)]) # times l_delta below + # Horner: acc r^n + the step's relations, the last one with weight 1 + r_pow = lambda j: one() if j == 0 else c['r' if j == 1 else f'r{j}'] + n = len(rels) + commit + terms = [(c['acc'], r_pow(n))] + [(v, r_pow(n - 1 - k)) for k, v in enumerate(rels)] + if commit: + acc2 = rd(terms + [(prod, ld), (c['e2'], m1())]) + e2 = c['e2'] + else: + acc2 = rd(terms) + e2 = rd([(prod, ld)]) + outs = [e2, acc2, x2, y2] for nm, v in zip(('e', 'acc', 'x', 'y'), outs): b[nm + '_out'] = max(b.get(nm + '_out', 0), v.hi) return fp.pack(outs) new = chain(fp, c, steps, final) - h2 = self.sha_add(h, [leaf[0], leaf[1][0], leaf[1][1]] + leaf[2:]) + h2 = self.sha_add(h, [leaf[0]] + ([leaf[1][0], leaf[1][1]] if commit else []) + ([leaf[-1]] if add else [])) out = be.pack([new, h2, be.subtree(list(run.values()))]) - return be.seq(be.checks([self.top2_zero(w) for w in fw]), out) + return be.seq(be.checks([self.top2_zero(w) for w in fw]), out) if commit else out return body + def qchunk_body(self, accs): + """Body of QCHUNK: ((qz, z^8), (z .. z^7)), 8 coefficients of Q -> qz z^8 + their polynomial at z. + Q(z) is this step from the top chunk down; the coefficients are checked below 2^254.""" + ((qz, z8), zp), q = accs + one, b = self.one(), self.b + out = self.rd([(Val(qz, b['qz']), Val(z8, KB)), (Val(q[0], F_HI), one)] + + [(Val(q[k], F_HI), Val(zp[k - 1], KB)) for k in range(1, 8)]) + b['qz_out'] = max(b.get('qz_out', 0), out.hi) + return self.be.seq(self.be.checks([self.top2_zero(w) for w in q]), out.e) + def sample_bounds(self): - """The bounds of the loop state: a fixed point of the three step bodies, found on the numeric - backend by widening the inputs (1% slack) until the outputs fit.""" + """The bounds of the loop state: a fixed point of the step bodies, found on the numeric backend by + widening the inputs (1% slack) until the outputs fit.""" b = self.b - b.update(e=KB, acc=KB, x=KB, y=KB) + b.update(e=KB, acc=KB, x=KB, y=KB, qz=KB) st = [[0] * 4, b'', [0] * len(RUN)] - leaf = [[0, 0], [[0] * 8, [0] * 4]] for _ in range(30): - for nm in ('e', 'acc', 'x', 'y'): + for nm in ('e', 'acc', 'x', 'y', 'qz'): b[nm + '_out'] = 0 - self.step_body('D')([[st, leaf], [0] * 8]) - self.step_body('A')([[st, leaf + [[0, 0]]], [[0] * 3, [0] * 8]]) - self.step_body('F')([[st, leaf + [[0, 0]]], [[0] * 2, [0] * 8]]) - if all(b[nm + '_out'] <= b[nm] for nm in ('e', 'acc', 'x', 'y')): + for kind, n_args in (('D', 0), ('A', 3), ('F', 2)): + for commit in (False, True): + leaf = [[0, 0]] + ([[[0] * 8, [0] * 4]] if commit else []) + ([[0, 0]] if n_args else []) + self.step_body(kind, commit)([[st, leaf], [[0] * n_args, [0] * 8] if n_args else [0] * 8]) + self.qchunk_body([[[0, 0], [0] * 7], [0] * 8]) + names = ('e', 'acc', 'x', 'y', 'qz') + if all(b[nm + '_out'] <= b[nm] for nm in names): return - for nm in ('e', 'acc', 'x', 'y'): + for nm in names: b[nm] = max(b[nm], b[nm + '_out'] * 101 // 100) raise RuntimeError('no fixed point for the loop bounds') @@ -430,11 +481,12 @@ def step_value(self, k, st, env): be = self.be kind = STEPS[k] data = [be.word(v) for v in self.data[k]] + fn = self.steps[(kind[0], COMMIT[k])] if kind == 'D': - return be.call(self.STEP_D, [env, data]) + return be.call(fn, [env, data]) run = dict(zip(RUN, st[2])) args = [be.subtree(run[nm]) if is_sym(be) else run[nm] for nm in Q_KEYS[kind]] - return be.call(self.STEP_A if kind in ('A+', 'A-') else self.STEP_F, [env, [args, data]]) + return be.call(fn, [env, [args, data]]) def loop(self, state0, leaves): be = self.be @@ -442,7 +494,7 @@ def loop(self, state0, leaves): for k, leaf in enumerate(leaves): state0 = self.step_value(k, state0, [state0, leaf]) return state0 - widths = [256 * leaf_words(kind) for kind in STEPS] + widths = [256 * leaf_words(kind, COMMIT[k]) for k, kind in enumerate(STEPS)] return be.comp(be.pair(state0, leaves), fold_leaves(be, self.state_shape(), widths, lambda k: lambda accs: self.step_value(k, accs[0], be.iden()))) @@ -556,8 +608,13 @@ def final(self, state, end, q, rz): (e, acc, x, y), h, run = state run = {nm: Val(w, KB) for nm, w in zip(RUN, run)} b = self.b - c = {'e': Val(e, b['e']), 'acc': Val(acc, b['acc']), 'x': Val(x, b['x']), 'y': Val(y, b['y']), - 'q': [Val(w, F_HI) for w in flat(q)], 'zp': [run[f'z{k}'] for k in range(1, 12)]} + chunks = [half for blk in q for half in ([blk] if len(blk) == 8 else [blk[:8], blk[8:]])] + ensure(len(blocks(N_Q)) == len(q) and all(len(ch) == 8 for ch in chunks), 'Q is read in chunks of 8 words') + qz = be.word(0) + for ch in reversed(chunks): + zs = [run[f'z{k}'].e for k in range(1, 8)] + qz = be.call(self.QCHUNK, [[[qz, run['z8'].e], zs], be.subtree(ch) if is_sym(be) else ch]) + c = {'e': Val(e, b['e']), 'acc': Val(acc, b['acc']), 'x': Val(x, b['x']), 'y': Val(y, b['y']), 'qz': Val(qz, b['qz'])} c.update({nm: Val(w, hi) for nm, w, hi in zip(END, end, self.end_hi)}) c.update({nm: run[nm] for nm in ('r', 'r2', 'r3', 'z6')}) @@ -565,28 +622,19 @@ def horner_end(c): g2 = rd([(c['x'], c['r2']), (c['X3'], fp.mul_const(c['r2'], -1)), (c['y'], c['r']), (c['Y3'], fp.mul_const(c['r'], -1))]) return rd([(c['acc'], c['r3']), (g2, one()), (c['e'], c['pis']), (c['ts'], m1())]) - def q_chunks(c): - q, zp = c['q'], c['zp'] - return [rd([(q[j], one())] + [(q[j + k], zp[k - 1]) for k in range(1, min(12, len(q) - j))]) for j in range(0, len(q), 12)] steps = [('let', ['s', 'z12'], lambda c: [horner_end(c), rd([(c['z6'], c['z6'])])]), - ('drop', ['acc', 'x', 'y', 'e', 'r', 'r2', 'r3'] + END), - ('let', ['chunks'], lambda c: [q_chunks(c)]), - ('drop', ['zp', 'q'])] + ('drop', ['acc', 'x', 'y', 'e', 'r', 'r2', 'r3'] + END)] def check(c, _): - qz = c['chunks'][-1] - for part in reversed(c['chunks'][:-1]): - qz = rd([(qz, c['z12']), (part, one())]) minus_m = fp.lin_const([(c['z12'], -1), (c['z6'], 18), (one(), -82)]) - return cv.zero_check(rd([(c['s'], one()), (qz, minus_m)])).e + return cv.zero_check(rd([(c['s'], one()), (c['qz'], minus_m)])).e identity = chain(fp, c, steps, check) r_hash = self.sha_final(h) z_hash = self.sha_final(self.sha_add(self.sha_add(self.sha_init(), [rz[0]]), list(q))) if not is_sym(be): ensure(r_hash == rz[0] and z_hash == rz[1], 'challenge') return 0 - return be.checks([identity, be.verify(be.eq256(r_hash, rz[0])), be.verify(be.eq256(z_hash, rz[1]))] + - [self.top2_zero(w) for w in flat(q)]) + return be.checks([identity, be.verify(be.eq256(r_hash, rz[0])), be.verify(be.eq256(z_hash, rz[1]))]) def program(self, inputs, pts, c, ci, sel, rz, leaves, q): """The whole check. inputs: {'L': (x, y) Vals, 'words': the public input accessors}; the rest as @@ -596,7 +644,7 @@ def program(self, inputs, pts, c, ci, sel, rz, leaves, q): if not is_sym(be): st0, end = s0 return self.final(self.loop(st0, leaves), end, q, rz) - lw = sum(256 * leaf_words(k) for k in STEPS) + lw = sum(256 * leaf_words(kind, COMMIT[k]) for k, kind in enumerate(STEPS)) qshape = [words(n) for n in blocks(N_Q)] parts = {} @@ -645,35 +693,41 @@ def bounds(): # --------------------------------------------------------------------------------------------------- # The witness of the check: ((points, (c, c^-1)), ((s, (r, z)), (leaves, Q))). c, c^-1: 8 + 4 words; # a leaf: (lambda, (f' 8 + 4 words)) or ((lambda, f'), d); Q in hashing blocks. -def leaf_shape(kind): - return [words(2), [words(8), words(4)]] + ([words(2)] if kind in ADDS else []) +def leaf_shape(kind, commit): + """(lambda, f', d): f' (8 + 4 words) if committed, d for an addition.""" + return [words(2)] + ([[words(8), words(4)]] if commit else []) + ([words(2)] if kind in ADDS else []) def witness_shape(): bit = lambda: E('', 1, 0) c = lambda: [words(8), words(4)] - return [[words(8), [c(), c()]], [[[bit(), bit()], words(2)], [[leaf_shape(k) for k in STEPS], [words(n) for n in blocks(N_Q)]]]] + leaves = [leaf_shape(kind, COMMIT[k]) for k, kind in enumerate(STEPS)] + return [[words(8), [c(), c()]], [[[bit(), bit()], words(2)], [leaves, [words(n) for n in blocks(N_Q)]]]] def witness_type(): T = lambda n: Tbalanced(['2^256'] * n) c = f'({T(8)} * {T(4)})' - leaf = lambda k: f'(({T(2)} * {c}) * {T(2)})' if k in ADDS else f'({T(2)} * {c})' - return (f'(({T(8)} * ({c} * {c})) * (((2 * 2) * {T(2)}) * ' - f'({Tbalanced([leaf(k) for k in STEPS])} * {Tbalanced([T(n) for n in blocks(N_Q)])})))') + leaf = lambda kind, commit: Tbalanced([T(2)] + ([c] if commit else []) + ([T(2)] if kind in ADDS else [])) + leaves = Tbalanced([leaf(kind, COMMIT[k]) for k, kind in enumerate(STEPS)]) + return f'(({T(8)} * ({c} * {c})) * (((2 * 2) * {T(2)}) * ({leaves} * {Tbalanced([T(n) for n in blocks(N_Q)])})))' def witness_bits(): - return 256 * (8 + 24 + 2 + N_Q + sum(leaf_words(k) for k in STEPS)) + 2 + return 256 * (8 + 24 + 2 + N_Q + sum(leaf_words(kind, COMMIT[k]) for k, kind in enumerate(STEPS))) + 2 def _split(w): - leaf = lambda lf: [lf[:2], [lf[2:10], lf[10:14]]] + ([lf[14:]] if len(lf) > 14 else []) + def leaf(k, lf): + parts = [lf[:2]] + if COMMIT[k]: + parts.append([lf[2:10], lf[10:14]]) + return parts + ([lf[-2:]] if STEPS[k] in ADDS else []) qs, i = [], 0 for n in blocks(N_Q): qs.append(w['q'][i:i + n]) i += n - return w['points'], [w['c'][:8], w['c'][8:]], [w['ci'][:8], w['ci'][8:]], [leaf(lf) for lf in w['leaves']], qs + return w['points'], [w['c'][:8], w['c'][8:]], [w['ci'][:8], w['ci'][8:]], [leaf(k, lf) for k, lf in enumerate(w['leaves'])], qs def witness_num(w): @@ -687,7 +741,7 @@ def witness_value(w): pts, c, ci, leaves, qs = _split(w) W = lambda ws: wbalanced([wv(256, x) for x in ws]) pair2 = lambda a: wpair(W(a[0]), W(a[1])) - leaf = lambda lf: balanced([W(lf[0]), pair2(lf[1])] + [W(x) for x in lf[2:]], wpair) + leaf = lambda lf: balanced([W(lf[0])] + [pair2(x) if len(x) == 2 and isinstance(x[0], list) else W(x) for x in lf[1:]], wpair) return wpair(wpair(W(pts), wpair(pair2(c), pair2(ci))), wpair(wpair(wpair(wbit(w['s'] >> 1), wbit(w['s'] & 1)), W([w['r'], w['z']])), wpair(wbalanced([leaf(lf) for lf in leaves]), wbalanced([W(x) for x in qs])))) diff --git a/tests/test_pairing.py b/tests/test_pairing.py index f3a8803..434af2c 100644 --- a/tests/test_pairing.py +++ b/tests/test_pairing.py @@ -57,12 +57,13 @@ def witness(L, w): def tampered(prover, w, how): """The honest values with one change, sealed again.""" v = {k: (list(x) if isinstance(x, list) else dict(x) if isinstance(x, dict) else x) for k, x in w['values'].items()} - v['f'] = [list(f) for f in v['f']] + v['f'] = [None if f is None else list(f) for f in v['f']] add = next(i for i, k in enumerate(PC.STEPS) if k in PC.ADDS) if how == 'slope': v['lam'][10] = v['lam'][10] + 1 elif how == 'Miller value': - v['f'][40][3] += 1 + k = next(i for i in range(40, PC.N_STEPS) if PC.COMMIT[i]) + v['f'][k][3] += 1 elif how == 'inverse': v['d'][add] = v['d'][add] + 1 elif how == 's':