feat(crypto): add Yul Groth16 pairing-precompile verification wrapper - #897
Merged
mijinummi merged 1 commit intoAug 20, 2026
Merged
Conversation
Adds `YulGroth16Verifier`, a library that stages a Groth16 proof, its verifying
key and the accumulated public-input commitment directly into one contiguous
memory buffer and hands it to the pairing precompile at 0x08, rather than
letting the compiler assemble the same call through repeated allocations and
stack shuffling.
Verification evaluates the standard equation
e(A, B) == e(alpha, beta) * e(vk_x, gamma) * e(C, delta)
rearranged into the single product the precompile checks
e(-A, B) * e(alpha, beta) * e(vk_x, gamma) * e(C, delta) == 1
with vk_x = IC[0] + sum(input[i] * IC[i+1]) accumulated via ecMul (0x07) and
ecAdd (0x06).
Implementation notes:
- The four pairs are written into a single 768-byte buffer; the free memory
pointer advances once for the whole call. The accumulator reuses one
224-byte scratch region across every public input instead of allocating per
operation.
- A is negated on the G1 side rather than negating B's Fp2 coordinates, and
(x, 0) is handled as its own negation.
- Parameters are flat fixed-size arrays. Solidity stores nested memory arrays
such as `uint256[2][2]` as pointers rather than inline words, which makes
offset arithmetic in Yul easy to get wrong; flat arrays keep the layout
explicit.
- Public inputs are rejected unless reduced mod the scalar field. The ecMul
precompile would otherwise reduce them silently, letting two distinct input
vectors verify against one proof.
- The function is `view`, not `pure`, because staticcall reads the environment.
Tests cover a valid proof, two distinct ways of breaking the equation, the
ecMul/ecAdd accumulation path, both revert paths, and gas. The fixtures are
built from BN254 generators so the pairing product is 1 by construction, which
exercises the whole precompile path without requiring a trusted setup to
produce circuit proofs.
Gas for a verification with one public input: 232,132. The pairing precompile
floor alone is 181,000 (45,000 + 4 * 34,000), plus ecMul, ecAdd and the 21,000
transaction base.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #848
Summary
Adds
contracts/crypto/YulGroth16Verifier.sol— a library that stages a Groth16 proof, its verifying key, and the accumulated public-input commitment directly into one contiguous memory buffer and hands it to the pairing precompile at0x08.Verification evaluates the standard equation
rearranged into the single product the precompile checks:
with
vk_x = IC[0] + sum(input[i] * IC[i+1])accumulated through ecMul (0x07) and ecAdd (0x06).Implementation notes
subversus two — and(x, 0)is handled as its own negation.uint256[2][2]as pointers rather than inline words, which makes Yul offset arithmetic quietly wrong. Flat arrays keep the layout explicit and match what getsmloaded.view, notpure—staticcallreads the environment. (contracts/lightclient/YulLightClient.solcurrently declarespurewhile doing exactly this, and does not compile.)Acceptance criteria
staticcallto0x08and evaluate the boolean in YulGas: 232,132 for a verification with one public input. The pairing precompile alone costs 45,000 + 4 × 34,000 = 181,000, plus ecMul (6,000), ecAdd (150) and the 21,000 transaction base — so the wrapper itself adds very little on top of the unavoidable floor.
Test fixtures
The fixtures are built from BN254 generators so that the pairing product is 1 by construction: with
A = alphaandB = betathe first two terms cancel, and withvk_xandCboth at infinity the remaining two are 1. That exercises the complete precompile path — negation, staging, the0x08call, and the ecMul/ecAdd accumulation — without needing a trusted setup to produce circuit proofs. Invalid cases perturbAandCindependently.If you would rather have fixtures from a real circuit, I am happy to add snarkjs-generated vectors in a follow-up; that means taking on a new dev dependency, so I did not do it unprompted.
npx hardhat compilefails onmainbefore it ever reaches this contract. I hit five pre-existing failures, none related to this change:contracts/bridge/BridgeGateway.sol:103pragma+ imports are pasted into the middle of a function bodycontracts/verifiers/YulEd25519Verifier.sol:98} else {inside assembly — Yul has noelsetest/relayer/MockGasTarget.sol:15,31let _ := i—_is reserved in Yulcontracts/bridge/YulBatchUnlocker.sol:34contracts/lightclient/YulLightClient.sol:97purefunction performing astaticcallThis matches CI, which is failing on all eight recent runs on
main.To validate this work I compiled and ran the tests against an isolated Hardhat config containing only this contract. All 7 tests pass. The harness was removed before committing — only the two intended files are in this diff.
One further note: the existing tests under
test/crypto/use the Hardhat 2 import style (import { ethers } from "hardhat"), which throwsdoes not provide an export named 'ethers'under the Hardhat 3 inpackage.json. This PR's test uses the Hardhat 3network.connect()API so it actually runs. It emits a deprecation warning suggestingnetwork.getOrCreate(); I keptconnect()as it is the documented test-time API, and I did not want to churn on an API I could not verify against the rest of the suite.I have deliberately not fixed any of the above — happy to open a separate PR or issues for them if useful, but they are well outside the scope of #848.