diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 01e3707db..24d2d12f4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -151,6 +151,125 @@ jobs: package: midnight-zkir args: --release --all-features + # --------------------------------------------------------------------- + # Solidity verifier generator (halo2_solidity_verifier). + # + # These three jobs are the per-PR gate. The heavy IVC proof benches and + # the release bytecode size/hash checks live in + # `.github/workflows/solidity_verifier_bench.yml`, which runs on pushes to + # `main`, on a weekly schedule, and on demand. + # + # Every job needs the pinned solc from + # `proofs/solidity-verifier/scripts/install_pinned_solc.sh`; the EVM jobs + # additionally need the Filecoin SRS that the rest of this workflow already + # caches at `zk_stdlib/examples/assets/bls_filecoin_2p19`, which is where + # the crate's `srs_dir()` looks by default. + # --------------------------------------------------------------------- + test-solidity-verifier: + if: github.event.pull_request.draft == false + name: Test Solidity verifier + runs-on: ubuntu-latest + + env: + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + # Codegen/layout tests compile Solidity; the heavy proof/EVM cases + # self-skip because HALO2_SOLIDITY_RUN_EVM_TESTS is unset here. + - name: Install pinned solc + run: | + echo "SOLC=$(proofs/solidity-verifier/scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + + - uses: ./.github/actions/cargo-test + with: + package: halo2_solidity_verifier + args: --all-features --all-targets -- --nocapture + + test-solidity-verifier-evm: + if: github.event.pull_request.draft == false + needs: download-srs + name: Test Solidity verifier real EVM + runs-on: ubuntu-latest + + env: + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + HALO2_SOLIDITY_RUN_EVM_TESTS: 1 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Restore SRS from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + with: + path: zk_stdlib/examples/assets/bls_filecoin_2p19 + key: fixed-srs-cache + + - name: Install pinned solc + run: | + echo "SOLC=$(proofs/solidity-verifier/scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + + - name: 'Install rust-toolchain.toml' + run: rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "ci" + + # Adversarial property tests: proof/VK/calldata mutations, non-canonical + # scalar and G1 rejection, EIP-170 runtime size, and the memoryguard + # overlap check against real compiled bytecode. + - name: Real EVM property tests + run: cargo test -p halo2_solidity_verifier --release --all-features pbt_ -- --nocapture + + # End-to-end: real proof -> render -> solc -> Prague revm verification. + - name: Poseidon fixture + run: | + cargo test -p halo2_solidity_verifier --release \ + --features evm,truncated-challenges --test poseidon_fixture -- --nocapture + + test-solidity-verifier-trace: + if: github.event.pull_request.draft == false + needs: download-srs + name: Test Solidity verifier trace equivalence + runs-on: ubuntu-latest + + env: + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + HALO2_SOLIDITY_RUN_EVM_TESTS: 1 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Restore SRS from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + with: + path: zk_stdlib/examples/assets/bls_filecoin_2p19 + key: fixed-srs-cache + + - name: Install pinned solc + run: | + echo "SOLC=$(proofs/solidity-verifier/scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + + - name: 'Install rust-toolchain.toml' + run: rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "ci" + + # Per-identity differential between the native Midfall verifier and the + # generated Solidity verifier. This is the only check that compares + # quotient VM semantics against the Rust verifier end to end. + - name: Poseidon native/Solidity trace equivalence + run: | + cargo test -p halo2_solidity_verifier --release \ + --features evm,truncated-challenges,rust-verifier-trace,solidity-trace \ + --lib native_midfall_verifier_trace_matches_solidity_trace -- --nocapture + doc-links: if: github.event.pull_request.draft == false name: Intra-doc links diff --git a/.github/workflows/solidity_verifier_bench.yml b/.github/workflows/solidity_verifier_bench.yml new file mode 100644 index 000000000..4194a340b --- /dev/null +++ b/.github/workflows/solidity_verifier_bench.yml @@ -0,0 +1,110 @@ +name: Solidity verifier IVC bench + +# The two IVC jobs below each generate a real recursive proof and take up to +# ~90 minutes, so they are deliberately not part of the per-PR gate in +# `ci.yaml`. The fast Solidity verifier jobs (codegen/layout tests, real EVM +# property tests, Poseidon fixture, Poseidon trace equivalence) run there on +# every pull request. +# +# Run these here on pushes to `main`, weekly, and on demand. Trigger a run +# against a branch from the Actions tab (`workflow_dispatch`) before merging a +# change to memory layout, proof layout, the quotient VM, or the templates. +on: + push: + branches: + - main + schedule: + # Mondays 04:00 UTC. + - cron: "0 4 * * 1" + workflow_dispatch: + +jobs: + full-ivc-bench: + name: Full IVC bench and release bytecode sizes + runs-on: ubuntu-latest + timeout-minutes: 90 + + env: + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + SRS_DIR: ${{ github.workspace }}/.srs + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + # `ensure_srs_assets.sh` fetches bls_filecoin_2p19 plus the + # midnight-srs 2p19/2p20/2p22 powers the decider proof needs. + - name: Cache SRS assets + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + with: + path: .srs + key: srs-solidity-verifier-ivc-v1 + + - name: Install pinned solc + run: | + echo "SOLC=$(proofs/solidity-verifier/scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + + - name: 'Install rust-toolchain.toml' + run: rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "ci" + + - name: Ensure SRS assets + run: proofs/solidity-verifier/scripts/ensure_srs_assets.sh + + - name: Full IVC bench + run: proofs/solidity-verifier/scripts/run_ivc_bench.sh --skip-srs-download + + # Fails the run when a generated runtime crosses EIP-170 or when the + # published verifier/VK/quotient runtime hashes drift from the values + # recorded in docs/reference/REPRODUCIBLE_BUILDS.md. + - name: Release bytecode size/hash checks + run: proofs/solidity-verifier/scripts/check_release_bytecode_sizes.sh + + - name: Upload IVC artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: ivc-keccak-solidity-dump + path: proofs/solidity-verifier/target/ivc-keccak-solidity-dump + + ivc-trace-equivalence: + name: IVC native/Solidity trace equivalence + runs-on: ubuntu-latest + timeout-minutes: 90 + + env: + SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc + SRS_DIR: ${{ github.workspace }}/.srs + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Cache SRS assets + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb + with: + path: .srs + key: srs-solidity-verifier-ivc-v1 + + - name: Install pinned solc + run: | + echo "SOLC=$(proofs/solidity-verifier/scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" + + - name: 'Install rust-toolchain.toml' + run: rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "ci" + + - name: Ensure SRS assets + run: proofs/solidity-verifier/scripts/ensure_srs_assets.sh + + # Compares the native Midfall verifier trace against the generated + # Solidity trace on the IVC decider proof, including quotient identity + # trace ids from the pinned external Halo2QuotientEvaluator. + - name: IVC trace equivalence + run: proofs/solidity-verifier/scripts/run_ivc_bench.sh --trace --skip-srs-download diff --git a/_to_delete/gitlock.HEAD.lock.27728 b/_to_delete/gitlock.HEAD.lock.27728 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.HEAD.lock.28476 b/_to_delete/gitlock.HEAD.lock.28476 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.HEAD.lock.31577 b/_to_delete/gitlock.HEAD.lock.31577 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.HEAD.lock.9194 b/_to_delete/gitlock.HEAD.lock.9194 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.ORIG_HEAD.lock.19008 b/_to_delete/gitlock.ORIG_HEAD.lock.19008 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.index.lock.11897 b/_to_delete/gitlock.index.lock.11897 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.index.lock.20480 b/_to_delete/gitlock.index.lock.20480 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.maintenance.lock.15666 b/_to_delete/gitlock.maintenance.lock.15666 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.maintenance.lock.31732 b/_to_delete/gitlock.maintenance.lock.31732 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/gitlock.maintenance.lock.32411 b/_to_delete/gitlock.maintenance.lock.32411 new file mode 100644 index 000000000..e69de29bb diff --git a/_to_delete/midfall-src.tar.gz b/_to_delete/midfall-src.tar.gz new file mode 100644 index 000000000..fdd81047b Binary files /dev/null and b/_to_delete/midfall-src.tar.gz differ diff --git a/_to_delete/pr1.git.patch b/_to_delete/pr1.git.patch new file mode 100644 index 000000000..604d3b036 --- /dev/null +++ b/_to_delete/pr1.git.patch @@ -0,0 +1,412 @@ +diff --git a/proofs/solidity-verifier/fixtures/ivc/README.md b/proofs/solidity-verifier/fixtures/ivc/README.md +--- a/proofs/solidity-verifier/fixtures/ivc/README.md ++++ b/proofs/solidity-verifier/fixtures/ivc/README.md +@@ -1,5 +1,15 @@ + # IVC Public-Accumulator Replay Fixture + ++> **STALE — regeneration required before any deployment.** These artifacts were ++> rendered before the MF-1 fix (`MODEXP_GAS` raised to the EIP-7883 bound and a ++> constructor modexp known-answer probe added), so the committed `.sol` files ++> here still carry the old 1360 bound and no modexp probe. They remain valid ++> inputs for the *replay* tests, which exercise verification logic rather than ++> the modexp bound, but they must be regenerated (and their provenance rows ++> below updated) on a host with the pinned solc before they are used as a ++> deployment source. Regeneration needs solc, which the environment that ++> applied the MF-1 fix did not have. ++ + Pre-rendered artifacts for `tests/ivc_accumulator_replay.rs`, which replays a + real IVC final proof and then mutates the accumulator public inputs to check the + decoder in `templates/partials/verifier/AccumulatorHelpers.yul` rejects them. +diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md +--- a/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md ++++ b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md +@@ -1,5 +1,15 @@ + # Moonlight Wrap point_pair Replay Fixture + ++> **STALE — regeneration required before any deployment.** These artifacts were ++> rendered before the MF-1 fix (`MODEXP_GAS` raised to the EIP-7883 bound and a ++> constructor modexp known-answer probe added), so the committed `.sol` files ++> here still carry the old 1360 bound and no modexp probe. They remain valid ++> inputs for the *replay* tests, which exercise verification logic rather than ++> the modexp bound, but they must be regenerated (and their provenance rows ++> below updated) on a host with the pinned solc before they are used as a ++> deployment source. Regeneration needs solc, which the environment that ++> applied the MF-1 fix did not have. ++ + Pre-rendered artifacts for the `point_pair` accumulator arm of + `tests/ivc_accumulator_replay.rs`. The IVC fixture next door covers + `AccumulatorEncoding::new` (explicit lhs/rhs scalars); this one covers +diff --git a/proofs/solidity-verifier/src/evm.rs b/proofs/solidity-verifier/src/evm.rs +--- a/proofs/solidity-verifier/src/evm.rs ++++ b/proofs/solidity-verifier/src/evm.rs +@@ -346,6 +346,19 @@ + /// are routed to revm's bundled implementations. The runner keeps an + /// `InMemoryDB` across calls so tests can deploy once and call many + /// times. ++ /// ++ /// MF-1 coverage gap, deliberately not papered over: this harness cannot ++ /// exercise a chain whose modexp is priced by EIP-7883. The pinned ++ /// revm 19 exposes `SpecId::OSAKA`, but that variant is Prague+EOF in this ++ /// version -- its `0x05` handler is `berlin_run`, i.e. EIP-2565 pricing ++ /// with the `/ 3` divisor -- so switching the spec here would produce ++ /// green tests that prove nothing about the repricing that bricked the ++ /// pre-fix bound. Raising real coverage needs a revm bump to a version ++ /// whose Osaka handler implements EIP-7883; until then, MF-1 is guarded ++ /// by `modexp_gas_bound_covers_every_live_schedule` (the bound is derived ++ /// from both EIP texts) and by the constructor's modexp known-answer ++ /// probe, which forwards the rendered bound and so fails at deployment on ++ /// any chain that prices modexp above it. + #[derive(Default)] + pub struct Evm { + db: InMemoryDB, +diff --git a/proofs/solidity-verifier/src/lowering/layout/mod.rs b/proofs/solidity-verifier/src/lowering/layout/mod.rs +--- a/proofs/solidity-verifier/src/lowering/layout/mod.rs ++++ b/proofs/solidity-verifier/src/lowering/layout/mod.rs +@@ -139,7 +139,7 @@ + + pub(crate) mod gas { + //! Exact gas schedule for the precompiles the generated verifier calls, +- //! from EIP-2537 (BLS12-381) and EIP-2565 (modexp). ++ //! from EIP-2537 (BLS12-381) and EIP-2565/EIP-7883 (modexp). + //! + //! A failing EIP-2537 or modexp call consumes ALL gas supplied to the + //! `STATICCALL`, so every generated call site forwards the exact scheduled +@@ -153,11 +153,19 @@ + //! amount succeeds by construction on any conformant implementation of + //! the current schedule. + //! +- //! Liveness caveat: if a future fork reprices these precompiles UPWARD, +- //! deployed verifiers start reverting on valid proofs and must be +- //! regenerated and redeployed. The constructor smoke probes forward the +- //! same bounds, so deployment onto an already-repriced chain fails fast +- //! instead of bricking at proof time. ++ //! Multi-schedule bounds: where more than one schedule is live across the ++ //! chains this verifier targets, the bound is the MAXIMUM over those ++ //! schedules rather than the one for a single fork (see ++ //! [`modexp_gas_word_frame`]). Over-forwarding costs nothing on success -- ++ //! unused gas is returned -- and only widens the burn of one *failing* ++ //! call by the difference, whereas under-forwarding bricks the verifier. ++ //! ++ //! Liveness caveat: if a future fork reprices these precompiles above the ++ //! bounds rendered here, deployed verifiers start reverting on valid ++ //! proofs and must be regenerated and redeployed. The constructor smoke ++ //! probes forward the same bounds for every precompile the runtime ++ //! depends on -- EIP-2537 *and* modexp (MF-1) -- so deployment onto an ++ //! already-repriced chain fails fast instead of bricking at proof time. + + /// EIP-2537 G1ADD flat cost. + pub(crate) const G1ADD_GAS: u64 = 375; +@@ -196,23 +204,65 @@ + 32_600 * pairs + 37_700 + } + +- /// EIP-2565 modexp cost for the only frame shape the verifier emits: +- /// 32-byte base, 32-byte exponent, 32-byte modulus. ++ /// Modexp bound for the only frame shape the verifier emits: 32-byte ++ /// base, 32-byte exponent, 32-byte modulus. ++ /// ++ /// Two schedules are live across the chains this verifier targets, so the ++ /// rendered bound is the maximum of both: ++ /// ++ /// * **EIP-2565** (Berlin): `max(200, multiplication_complexity * ++ /// iteration_count / 3)`. With `words = ceil(32/8) = 4` and ++ /// `multiplication_complexity = words^2 = 16`, that is ++ /// `max(200, 16 * 255 / 3) = 1360`. ++ /// * **EIP-7883** (Osaka/Fusaka): the `/ 3` divisor is **removed** and the ++ /// floor is raised to 500, giving `max(500, 16 * 255) = 4080`. ++ /// ++ /// MF-1: this function previously returned only the EIP-2565 value and ++ /// asserted that EIP-7883 "only reprices operands wider than 32 bytes". ++ /// That reading was wrong. EIP-7883 changes two independent things: the ++ /// `multiplication_complexity` branch for `max_length > 32` (`2 * words^2`, ++ /// which indeed does not apply here), *and* the removal of the `/ 3` ++ /// divisor from the final `multiplication_complexity * iteration_count` ++ /// product, which applies to every operand size. A verifier rendered with ++ /// the 1360 bound deploys fine on a repriced chain and then reverts ++ /// `PrecompileFailed` on every proof, because `staticcall` forwards a ++ /// fixed amount and the precompile runs out of gas inside the mandatory ++ /// Lagrange batch inversion. + /// +- /// `words = ceil(32/8) = 4`, `multiplication_complexity = words^2 = 16`, +- /// `iteration_count <= 255` (32-byte exponent), so +- /// `max(200, 16 * 255 / 3) = 1360`. EIP-7883 (scheduled for Osaka) keeps +- /// the same result for these operand sizes: it raises the floor to 500 +- /// (< 1360) and only reprices operands wider than 32 bytes. ++ /// `iteration_count` is the generic upper bound for any 32-byte exponent ++ /// (`exponent.bit_length() - 1 <= 255`). Both exponents the verifier ++ /// actually emits are `FR_MODULUS - 2`, whose 255-bit length gives 254 ++ /// iterations and an exact EIP-7883 price of 4064; the extra 16 gas keeps ++ /// the bound valid for any 32-byte exponent a future emitter might use. + pub(crate) const fn modexp_gas_word_frame() -> u64 { + const WORDS: u64 = 4; + const MULTIPLICATION_COMPLEXITY: u64 = WORDS * WORDS; ++ // Upper bound over every 32-byte exponent: `bit_length() - 1 <= 255`. + const MAX_ITERATION_COUNT: u64 = 255; +- let cost = MULTIPLICATION_COMPLEXITY * MAX_ITERATION_COUNT / 3; +- if cost < 200 { +- 200 ++ ++ // EIP-2565: divisor 3, floor 200. ++ let eip2565 = { ++ let cost = MULTIPLICATION_COMPLEXITY * MAX_ITERATION_COUNT / 3; ++ if cost < 200 { ++ 200 ++ } else { ++ cost ++ } ++ }; ++ // EIP-7883: no divisor, floor 500. ++ let eip7883 = { ++ let cost = MULTIPLICATION_COMPLEXITY * MAX_ITERATION_COUNT; ++ if cost < 500 { ++ 500 ++ } else { ++ cost ++ } ++ }; ++ ++ if eip2565 > eip7883 { ++ eip2565 + } else { +- cost ++ eip7883 + } + } + } +diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs +--- a/proofs/solidity-verifier/src/lowering/tests.rs ++++ b/proofs/solidity-verifier/src/lowering/tests.rs +@@ -1459,14 +1459,107 @@ + assert_eq!(gas::g1msm_gas(200), 200 * 12_000 * 519 / 1_000); + // 32600*k + 37700 for the verifier's two-pair check. + assert_eq!(gas::pairing_gas(2), 102_900); +- // EIP-2565, 32-byte base/exp/mod: max(200, 16 * 255 / 3). +- assert_eq!(gas::modexp_gas_word_frame(), 1_360); + // The rendered template constants come from the same module. + let constants = crate::lowering::render::TemplateConstants::default().gas; + assert_eq!(constants.g1add, 375); + assert_eq!(constants.g1msm_one_pair, 12_000); + assert_eq!(constants.pairing_two_pair, 102_900); +- assert_eq!(constants.modexp, 1_360); ++ assert_eq!(constants.modexp, gas::modexp_gas_word_frame()); ++} ++ ++/// MF-1: the modexp bound must cover EVERY live schedule, not just the one ++/// that happened to be current when the generator was written. Derive both ++/// prices here from their EIP texts instead of asserting one magic number, so ++/// the next repricing forces a conscious edit rather than a silent brick: ++/// `staticcall` forwards a fixed amount, so a bound below the chain's price ++/// makes the precompile OOG and every proof revert. ++#[test] ++fn modexp_gas_bound_covers_every_live_schedule() { ++ use crate::lowering::layout::gas; ++ ++ // Shared inputs for the only frame the verifier emits (32-byte base, ++ // exponent, and modulus). ++ const WORDS: u64 = 32_u64.div_ceil(8); ++ const MULTIPLICATION_COMPLEXITY: u64 = WORDS * WORDS; ++ // `exponent.bit_length() - 1` for a 32-byte exponent, upper-bounded. ++ const ITERATION_COUNT: u64 = 255; ++ ++ // EIP-2565: `max(200, multiplication_complexity * iteration_count / 3)`. ++ let eip2565 = std::cmp::max(200, MULTIPLICATION_COMPLEXITY * ITERATION_COUNT / 3); ++ assert_eq!(eip2565, 1_360, "EIP-2565 price for the 32-byte frame"); ++ ++ // EIP-7883: the `/ 3` divisor is removed for EVERY operand size (only the ++ // `2 * words^2` complexity branch is width-specific) and the floor rises ++ // to 500: `max(500, multiplication_complexity * iteration_count)`. ++ let eip7883 = std::cmp::max(500, MULTIPLICATION_COMPLEXITY * ITERATION_COUNT); ++ assert_eq!(eip7883, 4_080, "EIP-7883 price for the 32-byte frame"); ++ ++ assert_eq!( ++ gas::modexp_gas_word_frame(), ++ std::cmp::max(eip2565, eip7883), ++ "modexp bound must be the maximum over live schedules" ++ ); ++ assert!( ++ gas::modexp_gas_word_frame() >= eip7883, ++ "a bound below the EIP-7883 price bricks every proof on Osaka/Fusaka \ ++ chains: the fixed-gas staticcall OOGs inside the mandatory Lagrange \ ++ batch inversion and verifyProof reverts PrecompileFailed" ++ ); ++ ++ // The exponent the verifier actually emits is FR_MODULUS - 2 (255 bits, ++ // so 254 iterations); the generic bound must cover its exact price too. ++ assert!(gas::modexp_gas_word_frame() >= MULTIPLICATION_COMPLEXITY * 254); ++} ++ ++/// MF-1: modexp is the one precompile the runtime cannot do without -- the ++/// Lagrange batch inversion calls it on every proof -- and it was the one ++/// precompile the constructor never probed, so a stale bound deployed ++/// silently. Pin the probe's shape: right precompile, the pinned runtime ++/// bound (not `gas()`), a return-size check, and a known answer a stub ++/// cannot satisfy. ++#[test] ++fn constructor_probes_modexp_at_the_pinned_runtime_bound() { ++ let smoke = include_str!("../../templates/partials/verifier/PrecompileSmoke.sol"); ++ ++ assert!( ++ smoke.contains("staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}"), ++ "constructor must probe modexp at the same bound the runtime forwards" ++ ); ++ assert!( ++ smoke.contains( ++ "if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) }" ++ ), ++ "modexp probe must check the returned size" ++ ); ++ // 2^(r-2) == 2^-1, verified as mulmod(result, 2, r) == 1: a precompile ++ // that returns zeros, or echoes its input, fails this. ++ assert!( ++ smoke.contains("mstore(add(scratch, {{ template_constants.modexp.base_offset|hex() }}), 2)") ++ && smoke.contains( ++ "mstore(add(scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2))" ++ ) ++ && smoke.contains("if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) }"), ++ "modexp probe must run the runtime's own Fermat inversion as a known-answer test" ++ ); ++ ++ // Every precompile the runtime calls is now probed at its pinned bound. ++ for (probe, gas_constant) in [ ++ ("modexp.address", "MODEXP_GAS"), ++ ("eip2537.g1add_address", "G1ADD_GAS"), ++ ("eip2537.g1msm_address", "G1MSM_GAS_1PAIR"), ++ ("eip2537.pairing_address", "PAIRING_GAS_2PAIR"), ++ ] { ++ assert!( ++ smoke.contains(&format!( ++ "staticcall({gas_constant}, {{{{ template_constants.{probe}|hex() }}}}" ++ )), ++ "constructor smoke probe missing for {probe} at {gas_constant}" ++ ); ++ } ++ assert!( ++ !smoke.contains("staticcall(gas()"), ++ "smoke probes must forward pinned bounds, not gas()" ++ ); + } + + #[test] +@@ -1560,10 +1653,14 @@ + "generated verifier should include a deployment-time runtime prerequisite smoke test" + ); + for required in [ +- "Smoke-check the Cancun/EIP-2537 runtime features", ++ "Smoke-check the Cancun/EIP-2537/modexp runtime features", + "mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }})", + "eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)", + "non-Cancun fork fails during deployment", ++ // MF-1: modexp is a runtime prerequisite like any other precompile. ++ "modexp (0x05) known-answer probe at the pinned runtime bound", ++ "staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}", ++ "if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) }", + "G1ADD(identity, identity) -> identity", + "Known-answer probe: G1ADD(G, G) == 2G", + "template_constants.eip2537.g1_generator", +diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol +--- a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol ++++ b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol +@@ -169,7 +169,8 @@ + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = {{ memory.quotient_limb_comms_mptr_base }}; + + // ---------------------------------------------------------------------- +- // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. ++ // Precompile gas bounds: the scheduled EIP-2537 costs, and for modexp the ++ // maximum over the EIP-2565 and EIP-7883 schedules. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled +@@ -179,9 +180,17 @@ + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // +- // Liveness caveat: if a future fork reprices these precompiles UPWARD, +- // this verifier must be regenerated and redeployed. The constructor +- // smoke probes forward the same bounds, so deployment onto an ++ // MODEXP_GAS covers both live modexp schedules: EIP-2565 prices this ++ // frame at 1360, EIP-7883 (Osaka/Fusaka) removes the /3 divisor and ++ // prices it at 4080, so the larger bound is rendered. Forwarding the ++ // EIP-7883 bound on a pre-Osaka chain is free on success -- unused gas is ++ // returned -- while forwarding the EIP-2565 bound on a repriced chain ++ // reverts every proof. ++ // ++ // Liveness caveat: if a future fork reprices these precompiles above the ++ // bounds below, this verifier must be regenerated and redeployed. The ++ // constructor smoke probes forward the same bounds for EVERY precompile ++ // the runtime calls, modexp included, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = {{ template_constants.gas.g1add }}; +diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +--- a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol ++++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +@@ -1,9 +1,10 @@ +- /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. +- /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. +- /// The probes forward the same exact EIP-2537 gas bounds the runtime +- /// uses (see the gas-bound constants block), so a chain whose +- /// precompile schedule was repriced upward fails here, at deployment, +- /// instead of bricking verifyProof later. ++ /// @notice Smoke-check the Cancun/EIP-2537/modexp runtime features required by the verifier. ++ /// @dev Exercises MCOPY, modexp, and EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. ++ /// The probes forward the same exact gas bounds the runtime uses, for ++ /// every precompile it calls -- 0x05 modexp included (see the ++ /// gas-bound constants block) -- so a chain whose precompile schedule ++ /// was repriced above those bounds fails here, at deployment, instead ++ /// of bricking verifyProof later. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in +@@ -21,8 +22,39 @@ + mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }}) + if iszero(eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)) { revert(0, 0) } + ++ // ---------------------------------------------------------------- ++ // modexp (0x05) known-answer probe at the pinned runtime bound. ++ // ++ // MF-1: every other precompile the runtime calls was probed here, ++ // but modexp -- which the MANDATORY Lagrange batch inversion and ++ // every scalar_inv call depend on -- was not. Two live schedules ++ // price this frame differently (EIP-2565: 1360, EIP-7883: 4080), ++ // and a bound below the chain's price does not degrade: the ++ // staticcall forwards a fixed amount, the precompile OOGs, and ++ // EVERY proof reverts PrecompileFailed. Without this probe that ++ // failure is invisible until the first verifyProof call, on a ++ // contract that deployed cleanly. ++ // ++ // The vector is the runtime's own operation -- Fermat inversion ++ // in Fr -- so it exercises the exact frame shape, exponent width, ++ // and gas bound used at proof time: 2^(FR_MODULUS - 2) == 2^-1. ++ // Checking mulmod(result, 2, FR_MODULUS) == 1 rather than a ++ // rendered constant keeps the probe self-contained while still ++ // rejecting a stub: a precompile returning zeros (or its input) ++ // fails, since 0 * 2 != 1 mod r. ++ // ---------------------------------------------------------------- ++ mstore(add(scratch, {{ template_constants.modexp.base_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // base len ++ mstore(add(scratch, {{ template_constants.modexp.exp_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // exp len ++ mstore(add(scratch, {{ template_constants.modexp.mod_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // mod len ++ mstore(add(scratch, {{ template_constants.modexp.base_offset|hex() }}), 2) ++ mstore(add(scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2)) ++ mstore(add(scratch, {{ template_constants.modexp.mod_offset|hex() }}), FR_MODULUS) ++ if iszero(staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, scratch, {{ template_constants.modexp.frame_bytes|hex() }}, scratch, {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) } ++ + // Start the EIP-2537 probes with the identity encoding for G1/G2: +- // all-zero padded words. ++ // all-zero padded words. This also clears the modexp frame above. + for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { + mstore(add(scratch, off), 0) + } +diff --git a/proofs/solidity-verifier/tests/template_digest.rs b/proofs/solidity-verifier/tests/template_digest.rs +--- a/proofs/solidity-verifier/tests/template_digest.rs ++++ b/proofs/solidity-verifier/tests/template_digest.rs +@@ -13,7 +13,7 @@ + + /// keccak over the sorted (path, length, content) stream of `templates/`. + const EXPECTED_TEMPLATE_TREE_DIGEST: &str = +- "0x0809729cd0ee884736704458fbc53a2afc62c0de675b3a749b3c22c9ac797671"; ++ "0xfe89267c0778787678b938eb6f56f5e79f4bc11578786da45f2aad82384349d9"; + + fn collect_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("template directory is readable") { diff --git a/_to_delete/pr2.git.patch b/_to_delete/pr2.git.patch new file mode 100644 index 000000000..a165c8421 --- /dev/null +++ b/_to_delete/pr2.git.patch @@ -0,0 +1,739 @@ +diff --git a/proofs/solidity-verifier/src/lowering/layout/memory.rs b/proofs/solidity-verifier/src/lowering/layout/memory.rs +--- a/proofs/solidity-verifier/src/lowering/layout/memory.rs ++++ b/proofs/solidity-verifier/src/lowering/layout/memory.rs +@@ -528,6 +528,14 @@ + /// selector accumulators are accounted for. + pub(crate) quotient_tmp_mptr: usize, + pub(crate) quotient_stack_mptr: usize, ++ /// First address past the quotient VM stack / callback scratch region. ++ /// ++ /// MF-3: the interpreter's spill pointer walks upward from ++ /// `quotient_stack_mptr` with no ceiling of its own, so the rendered VM ++ /// clamps `q_sp` against this bound. The region is sized for the larger ++ /// of the interpreted stack depth and the structured native-callback ++ /// scratch, which is exactly the ceiling both uses must respect. ++ pub(crate) quotient_stack_hi: usize, + pub(crate) pcs_q_eval_source_table_mptr: usize, + pub(crate) pcs_q_com_trace_scratch_mptr: usize, + pub(crate) pcs_final_msm_scratch_mptr: usize, +@@ -1099,6 +1107,7 @@ + lagrange_denoms_mptr, + quotient_tmp_mptr, + quotient_stack_mptr, ++ quotient_stack_hi: quotient_stack_mptr + quotient_stack_len.max(MODEXP_FRAME_BYTES), + pcs_q_eval_source_table_mptr, + pcs_q_com_trace_scratch_mptr, + pcs_final_msm_scratch_mptr, +diff --git a/proofs/solidity-verifier/src/lowering/quotient.rs b/proofs/solidity-verifier/src/lowering/quotient.rs +--- a/proofs/solidity-verifier/src/lowering/quotient.rs ++++ b/proofs/solidity-verifier/src/lowering/quotient.rs +@@ -187,6 +187,8 @@ + selector_max_power: selector_fold.max_power, + selector_tail_updates: Self::selector_tail_updates(selector_fold), + stack_mptr: quotient_stack_mptr, ++ stack_hi: memory.quotient_stack_hi, ++ num_consts: build.consts.len(), + program_mptr, + operand_lo: operand_bounds.0, + operand_hi: operand_bounds.1, +diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs +--- a/proofs/solidity-verifier/src/lowering/render/models.rs ++++ b/proofs/solidity-verifier/src/lowering/render/models.rs +@@ -706,6 +706,17 @@ + pub(crate) selector_tail_updates: Vec, + /// Operand stack / callback scratch base. + pub(crate) stack_mptr: usize, ++ /// First address past the operand stack / callback scratch region. ++ /// ++ /// MF-3: spill sites clamp `q_sp` against this so a malformed program ++ /// cannot walk the stack pointer out of its registered region. ++ pub(crate) stack_hi: usize, ++ /// Number of Fr words in the generated quotient constant table. ++ /// ++ /// MF-3: constant-table indexes decoded from program bytes are clamped ++ /// against this so an out-of-range index cannot read program bytes (or ++ /// commitment words) as field constants. ++ pub(crate) num_consts: usize, + /// Memory pointer to the first encoded program word. + pub(crate) program_mptr: usize, + /// Lowest word address a VM memory operand may load from (P12/L-6). +@@ -1218,6 +1229,8 @@ + selector_max_power: 0, + selector_tail_updates: vec![], + stack_mptr: 0, ++ stack_hi: usize::MAX, ++ num_consts: usize::MAX, + program_mptr: 0, + // Synthetic model: a permissive clamp window keeps the + // rendered guards inert for layout-shape tests. +diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs +--- a/proofs/solidity-verifier/src/lowering/tests.rs ++++ b/proofs/solidity-verifier/src/lowering/tests.rs +@@ -1511,6 +1511,176 @@ + assert!(gas::modexp_gas_word_frame() >= MULTIPLICATION_COMPLEXITY * 254); + } + ++/// MF-4: the typed taxonomy exists so an incident responder can tell a chain ++/// fault from a rejected proof. Three paths used to conflate them -- a failed ++/// precompile inside the accumulator precheck or the final pairing surfaced as ++/// an input rejection, and a zero Lagrange denominator (a transcript event) ++/// surfaced as `PrecompileFailed`. Pin the split so it cannot regress. ++#[test] ++fn precompile_faults_and_input_rejections_use_distinct_selectors() { ++ let corpus = verifier_template_corpus(); ++ ++ // (a) The pairing helper: staticcall/returndatasize failure is a chain ++ // fault; a pairing that ran and returned != 1 rejects the proof. ++ assert!( ++ corpus.contains( ++ "if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) }\n // Compare against 1 rather than truncating to the low bit:" ++ ), ++ "ec_pairing must report a failed pairing staticcall as PrecompileFailed" ++ ); ++ assert!( ++ corpus.contains("ret := eq(mload(scratch), 1)\n if iszero(ret) { fail(ERR_PROOF_REJECTED) }"), ++ "ec_pairing must report a pairing result of 0 as ProofRejected" ++ ); ++ ++ // (b) batch_invert and the accumulator validator both carry a cause flag ++ // rather than a bare boolean. ++ assert!( ++ corpus.contains( ++ "function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret, precompile_failed" ++ ), ++ "batch_invert must report whether its modexp call failed" ++ ); ++ assert!( ++ corpus ++ .contains("function validate_public_accumulator(success, r) -> out, precompile_failed"), ++ "the accumulator validator must report whether its G1MSM call failed" ++ ); ++ ++ // (c) Both boundaries must branch on that flag. ++ for (guarded, fallback, what) in [ ++ ( ++ "if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }", ++ "fail(ERR_PROOF_REJECTED)", ++ "Lagrange", ++ ), ++ ( ++ "if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }", ++ "fail(ERR_BAD_POINT_ENCODING)", ++ "accumulator", ++ ), ++ ] { ++ assert!( ++ corpus.contains(guarded) && corpus.contains(fallback), ++ "{what} boundary must split precompile faults from rejected input" ++ ); ++ } ++ ++ // A zero denominator means the squeezed x hit a domain point: an input ++ // rejection, not a broken chain. ++ assert!( ++ !corpus.contains( ++ "success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r)" ++ ), ++ "the Lagrange batch inversion must thread its failure cause" ++ ); ++} ++ ++/// MF-3: the quotient VM's terminal checks (`q_pc == q_end`, `q_has_top == 0`, ++/// `q_sp == base`) cannot see three failure shapes, so the interpreter grew ++/// guards for each. The program is VK-codehash-pinned, so none of these are ++/// reachable on-chain with a well-formed artifact -- they are containment for ++/// a future generator bug, mirroring on the deployed side what the reference ++/// VM already enforces at build time (`stack.len() == 1` per identity, ++/// `const_at` bounds, and `identity_segment` rejecting a native marker inside ++/// an expression). ++#[test] ++fn quotient_vm_interpreter_fails_closed_on_malformed_programs() { ++ let vm = include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"); ++ ++ // (a) A fold with no live cached top would re-fold a STALE q_top, and ++ // both terminal checks would still pass. ++ assert_eq!( ++ vm.matches("{%- call q_top_guard() %}").count(), ++ 2, ++ "both FOLD_MAIN and FOLD_SELECTOR must require a live cached top" ++ ); ++ assert!( ++ vm.contains("if iszero(q_has_top) { q_program_fail() }"), ++ "q_top_guard must fail closed when the cached top is not live" ++ ); ++ ++ // (b) Native callbacks used to RESET q_sp, which silently discarded ++ // operands spilled by a preceding partial expression -- an identity would ++ // drop out of nu_y(x) with the program still ending balanced. Assert ++ // instead of reset. ++ // The only assignment of the stack base to q_sp may be its declaration. ++ assert_eq!( ++ vm.matches("q_sp := {{ program.stack_mptr|hex() }}").count(), ++ 1, ++ "native callbacks must not reset q_sp; a reset hides dropped operands" ++ ); ++ assert!( ++ vm.contains("let q_sp := {{ program.stack_mptr|hex() }}"), ++ "the surviving q_sp assignment must be its initial declaration" ++ ); ++ assert_eq!( ++ vm.matches("{%- call q_stack_empty_guard() %}").count(), ++ 3, ++ "each native callback boundary must assert an already-empty stack" ++ ); ++ ++ // (c) The spill pointer has no ceiling of its own. ++ assert_eq!( ++ vm.matches("if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() }") ++ .count(), ++ 10, ++ "every cached-top spill site must clamp q_sp to its registered region" ++ ); ++ ++ // (d) u16 constant-table indexes reach far outside the pinned payload, so ++ // unlike the u8 forms they cannot rely on bounded drift. ++ assert_eq!( ++ vm.matches("{%- call q_const_guard(\"qconst\") %}").count(), ++ 3, ++ "u16 constant-table indexes must be clamped to the rendered table length" ++ ); ++ assert!( ++ vm.contains("if iszero(lt({{ idx }}, {{ program.num_consts }})) { q_program_fail() }"), ++ "q_const_guard must clamp against the generated constant-table length" ++ ); ++} ++ ++/// MF-2: the free-memory-pointer guard is the only on-chain check that solc's ++/// stack-spill reservation has not grown into the generated absolute layout. ++/// A fork that recompiles at a different (version, optimiser-runs) pair can ++/// still fit EIP-170, deploy, and then revert on every proof -- so the guard ++/// must say *why* rather than reverting bare, which is indistinguishable from ++/// every other empty revert. ++#[test] ++fn memory_layout_guard_reverts_with_a_typed_selector() { ++ let verifier_template = include_str!("../../templates/contracts/Halo2Verifier.sol"); ++ let smoke_template = include_str!("../../templates/partials/verifier/PrecompileSmoke.sol"); ++ ++ for (source, name) in [ ++ (verifier_template, "verifyProof"), ++ (smoke_template, "constructor"), ++ ] { ++ assert!( ++ source.contains("mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED))") ++ && source.contains("revert(0x00, 0x04)"), ++ "{name} memory-layout guard must revert with MemoryLayoutViolated()" ++ ); ++ } ++ // The guard must still be the first thing each assembly block does: it ++ // protects the writes that follow, so a bare `revert(0, 0)` left behind ++ // on either path means the typed rewrite missed a site. ++ assert!( ++ verifier_template.contains("if gt(mload(0x40), TRANSCRIPT_MPTR) {"), ++ "runtime guard must still compare the FMP against TRANSCRIPT_MPTR" ++ ); ++ assert!( ++ !verifier_template.contains("if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) }"), ++ "runtime memory-layout guard still reverts bare" ++ ); ++ assert!( ++ !smoke_template.contains( ++ "if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { revert(0, 0) }" ++ ), ++ "constructor memory-layout guard still reverts bare" ++ ); ++} ++ + /// MF-1: modexp is the one precompile the runtime cannot do without -- the + /// Lagrange batch inversion calls it on every proof -- and it was the one + /// precompile the constructor never probed, so a stale bound deployed +@@ -1572,7 +1742,7 @@ + "ABI/proof length/instance shape checks should fail before accumulator or transcript parsing" + ); + assert!( +- verifier_template.contains("success := validate_public_accumulator(success, r)\n if iszero(success) { fail(ERR_BAD_POINT_ENCODING) }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), ++ verifier_template.contains("success, acc_precompile_failed := validate_public_accumulator(success, r)\n if iszero(success) {\n // MF-4: a G1MSM that could not run at all is a chain fault,\n // not a malformed accumulator point.\n if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }\n fail(ERR_BAD_POINT_ENCODING)\n }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), + "accumulator precheck should fail before transcript parsing" + ); + assert!( +@@ -1583,7 +1753,7 @@ + ); + assert!( + verifier_template.contains( +- "if iszero(success) { fail(ERR_PRECOMPILE_FAILED) }\n\n {%- match quotient_external %}" ++ "if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }\n fail(ERR_PROOF_REJECTED)\n }\n\n {%- match quotient_external %}" + ), + "failed Lagrange/common-polynomial setup should fail before quotient reconstruction" + ); +@@ -1843,9 +2013,9 @@ + let verifier_template = verifier_template_corpus(); + + for required in [ +- "function validate_public_accumulator(success, r) -> out", ++ "function validate_public_accumulator(success, r) -> out, precompile_failed", + "Fail malformed accumulator public inputs before transcript", +- "success := validate_public_accumulator(success, r)", ++ "success, acc_precompile_failed := validate_public_accumulator(success, r)", + "gas_checkpoint(2) // after VK loading + accumulator public-input precheck", + "Batch the prevalidated public IVC accumulator pairing equation", + ] { +@@ -2399,8 +2569,14 @@ + verifier_template.contains("function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret") + && verifier_template + .contains("ret := success\n if iszero(ret) { leave }") ++ // MF-4: a failed/short-returning pairing staticcall is a chain ++ // fault (PrecompileFailed); only a pairing that RAN and ++ // returned != 1 rejects the proof. ++ && verifier_template.contains( ++ "ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }}))\n if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) }", ++ ) + && verifier_template.contains( +- "ret := and(ret, eq(mload(scratch), 1))\n if iszero(ret) { fail(ERR_PROOF_REJECTED) }\n ret := 1", ++ "ret := eq(mload(scratch), 1)\n if iszero(ret) { fail(ERR_PROOF_REJECTED) }\n ret := 1", + ), + "final pairing helper must revert on pairing failure and normalize success to one" + ); +@@ -2487,6 +2663,10 @@ + ("PrecompileFailed()", "ERR_PRECOMPILE_FAILED"), + ("ProofRejected()", "ERR_PROOF_REJECTED"), + ("QuotientProgramInvalid()", "ERR_QUOTIENT_PROGRAM_INVALID"), ++ // MF-2: the memory-layout guard reports a build fault (a recompile ++ // whose spill region reaches the generated layout), so it must be ++ // decodable rather than an anonymous empty revert. ++ ("MemoryLayoutViolated()", "ERR_MEMORY_LAYOUT_VIOLATED"), + ] { + let digest = Keccak256::digest(signature.as_bytes()); + let selector = format!( +diff --git a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +--- a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol ++++ b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +@@ -50,7 +50,10 @@ + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. +- // Constructor smoke probes intentionally keep bare reverts. ++ // Constructor smoke probes intentionally keep bare reverts -- they report ++ // a chain-capability failure, and the deployment transaction identifies ++ // itself. The one constructor exception is the memory-layout guard ++ // (MF-2), which reports a BUILD fault and is typed on both paths. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). +@@ -72,6 +75,12 @@ + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); ++ /// @notice solc's stack-spill reservation overlaps the generated absolute ++ /// memory layout. This is a BUILD fault, not a proof fault: the ++ /// artifact was compiled with a (version, optimiser) pair whose ++ /// free-memory pointer starts at or above TRANSCRIPT_MPTR, so no ++ /// input can ever verify. Redeploy from the pinned toolchain. ++ error MemoryLayoutViolated(); + + {% include "partials/verifier/Constants.sol" %} + +@@ -159,7 +168,18 @@ + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. +- if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } ++ // ++ // MF-2: this is the only on-chain guard against a recompile that ++ // silently moves the spill region, and the failure it catches is ++ // permanent (no input can verify). `fail()` is not in scope this ++ // early, so write the MemoryLayoutViolated() selector inline ++ // rather than reverting bare -- an empty revert here is ++ // indistinguishable from every other empty revert, which is ++ // exactly the wrong signal for a build fault. ++ if gt(mload(0x40), TRANSCRIPT_MPTR) { ++ mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED)) ++ revert(0x00, 0x04) ++ } + + // This block owns the call-frame memory and remains terminal. + // Generated scratch starts at TRANSCRIPT_MPTR, preserving +diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul +--- a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul ++++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul +@@ -9,7 +9,10 @@ + sub(ptr, lo) > (hi - lo) cover both bounds (~6 gas per operand). + u8 constant-table indexes stay unguarded: their drift is bounded to + 0x1FE0 bytes inside the VK-reserved region and covered by build-time +- validate_quotient_const_slots. ++ validate_quotient_const_slots. MF-3: the u16 forms do NOT share that ++ argument -- an out-of-range u16 index reaches 0x1FFFE0 bytes past the ++ table, well outside the pinned payload -- so those three sites are ++ clamped against the rendered table length. + -#} + {%- macro q_ptr_guard(ptr) %} + if gt(sub({{ ptr }}, {{ program.operand_lo|hex() }}), {{ (program.operand_hi - program.operand_lo)|hex() }}) { q_program_fail() } +@@ -23,6 +26,35 @@ + {%- macro q_pop_guard() %} + if eq(q_sp, {{ program.stack_mptr|hex() }}) { q_program_fail() } + {%- endmacro %} ++{#- ++ MF-3 interpreter fail-closed guards. The program is trusted through the VK ++ codehash pin, so none of these are reachable on-chain with a well-formed ++ artifact; they are containment for a future GENERATOR bug, in the same ++ spirit as the P12 operand clamps above. Each closes a hole the terminal ++ end-of-program checks provably cannot see: ++ ++ - q_top_guard: FOLD_MAIN/FOLD_SELECTOR consume the cached top. With ++ q_has_top clear, the fold silently re-folds a STALE q_top and both ++ terminal checks (q_has_top == 0, q_sp == base) still pass. ++ - q_stack_empty_guard: native callbacks used to RESET q_sp to the base ++ rather than assert it, so operands spilled by a preceding partial ++ expression were discarded with no trace -- an identity would drop out ++ of nu_y(x) while the program still ended balanced. ++ - q_const_guard: constant-table indexes are decoded from program bytes ++ and were unclamped, so an out-of-range index reads whatever follows the ++ table (program bytes, commitments) as an Fr constant. ++ - the inlined spill-ceiling check at every push site: q_sp walks ++ upward with no ceiling of its own. ++-#} ++{%- macro q_top_guard() %} ++ if iszero(q_has_top) { q_program_fail() } ++{%- endmacro %} ++{%- macro q_stack_empty_guard() %} ++ if iszero(eq(q_sp, {{ program.stack_mptr|hex() }})) { q_program_fail() } ++{%- endmacro %} ++{%- macro q_const_guard(idx) %} ++ if iszero(lt({{ idx }}, {{ program.num_consts }})) { q_program_fail() } ++{%- endmacro %} + // =============================================================== + // Batched identity numerator / linearization target. + // +@@ -473,9 +505,11 @@ + // Fr words, so shl(5, const_idx) converts an index to + // a byte offset. + let qconst := shr(240, mload(q_pc)) ++{%- call q_const_guard("qconst") %} + // Push semantics: spill the old cached top, if any, + // then install the loaded constant as the new q_top. + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -496,6 +530,7 @@ + // Load one canonical Fr word from generated memory and + // push it through the cached-top stack discipline. + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -544,6 +579,7 @@ + // impossible for valid generated bytecode. + default { q_program_fail() } + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -595,6 +631,7 @@ + default { q_program_fail() } + {%- call q_ptr_guard("q_ptr") %} + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -612,6 +649,7 @@ + q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_ptr") %} + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -662,6 +700,7 @@ + // constant table has fewer than 256 referenced slots. + let qconst := byte(0, mload(q_pc)) + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -697,6 +736,7 @@ + // constant tables. + let qconst := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) ++{%- call q_const_guard("qconst") %} + let q_const_ptr := add(q_const_mptr, shl(5, qconst)) + {%- call q_ptr_guard("q_const_ptr") %} + q_top := addmod(q_top, mload(q_const_ptr), r) +@@ -708,6 +748,7 @@ + // Two-byte constant-index multiply. + let qconst := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) ++{%- call q_const_guard("qconst") %} + let q_const_ptr := add(q_const_mptr, shl(5, qconst)) + {%- call q_ptr_guard("q_const_ptr") %} + q_top := mulmod(q_top, mload(q_const_ptr), r) +@@ -918,6 +959,7 @@ + // u16 ptr} pairs. The result is pushed as a fresh + // stack value. + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -956,6 +998,7 @@ + {%- call q_ptr_guard("q_lhs") %} + let q_lhs_value := mload(q_lhs) + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -1003,6 +1046,7 @@ + let q_coeff_pc := q_pc + q_pc := add(q_pc, {{ template_constants.quotient_vm.limb_pairwise_coeffs }}) + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -1081,6 +1125,7 @@ + q_pc := add(q_pc, 5) + + if q_has_top { ++ if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } +@@ -1214,7 +1259,7 @@ + // stack. The Rust memory planner must reserve enough + // words for structured_permutation_scratch_words(meta) + // whenever this opcode can appear. +- q_sp := {{ program.stack_mptr|hex() }} ++{%- call q_stack_empty_guard() %} + // The generated lines below call the same fold snippets + // used by interpreted expressions, so trace IDs and + // y-batch positions remain contiguous. +@@ -1240,7 +1285,7 @@ + // f+beta/prefix/suffix scratch rather than as a + // conventional VM stack. The Rust memory planner must + // reserve structured_lookup_scratch_words(meta). +- q_sp := {{ program.stack_mptr|hex() }} ++{%- call q_stack_empty_guard() %} + // Generated LogUp code follows the same y-batch order + // as the Rust identity stream. + {%- for line in quotient_native_lookup_computation %} +@@ -1265,7 +1310,7 @@ + // interpreter stack before dispatching. + q_top := 0 + q_has_top := 0 +- q_sp := {{ program.stack_mptr|hex() }} ++{%- call q_stack_empty_guard() %} + // Native identity sub-cases are generated from selected heavy gate identities. + switch q_native_idx + {%- for code_block in quotient_native_identity_computations %} +@@ -1283,6 +1328,7 @@ + case {{ template_constants.quotient_vm.op.fold_main|hex() }} { + // q_top is the complete value of one fully evaluated + // identity at x. It leaves the expression stack here. ++{%- call q_top_guard() %} + let q_eval := q_top + q_has_top := 0 + {%- if self.trace %} +@@ -1312,6 +1358,7 @@ + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, {{ program.num_selector_buckets }})) { q_program_fail() } + if gt(q_sel_gap, {{ program.selector_max_power|hex() }}) { q_program_fail() } ++{%- call q_top_guard() %} + let q_eval := q_top + q_has_top := 0 + {%- if self.trace %} +diff --git a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul +--- a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul ++++ b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul +@@ -248,7 +248,13 @@ + // carried-scalar and fixed-base-tail arms; renders whose + // accumulator layout has neither (e.g. point_pair with no tail) + // legally leave it unused. +- function validate_public_accumulator(success, r) -> out { ++ // MF-4: `precompile_failed` separates a G1MSM staticcall that ++ // could not run (chain/gas fault) from a public-input point this ++ // verifier decoded and rejected (bad packing, out-of-field ++ // coordinate, non-canonical identity encoding, or a point the ++ // precompile found off-curve/out-of-subgroup). Both fail closed at ++ // the call site; only the second is a BadPointEncoding. ++ function validate_public_accumulator(success, r) -> out, precompile_failed { + out := success + let bits := {{ self.expected_num_acc_limb_bits }} + let n := {{ self.expected_num_acc_limbs }} +@@ -303,6 +309,7 @@ + // is also a curve/subgroup validation round-trip. + out := staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) ++ precompile_failed := iszero(out) + } + } + +@@ -418,6 +425,7 @@ + {{ template_constants.g1_bytes|hex() }} + ) + out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) ++ precompile_failed := iszero(out) + } + } + // The caller checks `out` and reverts before transcript work if +diff --git a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul +--- a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul ++++ b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul +@@ -138,7 +138,16 @@ + // The function returns a boolean instead of reverting so callers + // can combine it with other `success` plumbing until a section + // boundary decides whether to fail closed. +- function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { ++ // ++ // MF-4: the second return value separates a FAILED PRECOMPILE ++ // (staticcall reverted / OOG'd / returned the wrong size -- a ++ // chain or gas-schedule fault) from a REJECTED INPUT (a zero or ++ // non-canonical denominator, which for the Lagrange batch means ++ // the squeezed x landed on a domain point). Both fail closed at ++ // the section boundary, but they are different incidents and used ++ // to surface under the same PrecompileFailed selector, pointing ++ // responders at the node when the transcript was the cause. ++ function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret, precompile_failed { + ret := success + if iszero(ret) { leave } + // Memory ranges must be forward and word-aligned by +@@ -177,6 +186,7 @@ + mstore(add(single_scratch, {{ template_constants.modexp.mod_offset|hex() }}), r) + ret := staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, single_scratch, {{ template_constants.modexp.frame_bytes|hex() }}, single_scratch, {{ template_constants.modexp.output_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) ++ precompile_failed := iszero(ret) + if ret { mstore(mptr_start, mload(single_scratch)) } + leave + } +@@ -227,6 +237,7 @@ + mstore(add(gp_mptr, {{ template_constants.modexp.mod_offset|hex() }}), r) + ret := staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, gp_mptr, {{ template_constants.modexp.frame_bytes|hex() }}, gp_mptr, {{ template_constants.modexp.output_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) ++ precompile_failed := iszero(ret) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would +@@ -274,13 +285,20 @@ + mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) + mcopy(add(scratch, 0x180), rhs_mptr, 0x80) + mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) ++ // MF-4: separate "the chain could not run the pairing" from ++ // "the pairing ran and rejected this proof". Both fail closed, ++ // but they are different incidents: the first points at the ++ // node/fork (a missing, repriced, or short-returning ++ // precompile), the second at the proof. Collapsing them into ++ // ProofRejected sent every responder looking at the wrong one. + ret := staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) + ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) ++ if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. +- ret := and(ret, eq(mload(scratch), 1)) ++ ret := eq(mload(scratch), 1) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } + ret := 1 + } +diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol +--- a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol ++++ b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol +@@ -232,6 +232,7 @@ + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; ++ uint256 internal constant ERR_MEMORY_LAYOUT_VIOLATED = 0xc9888d23; + + // BLS12-381 scalar-field modulus, used for transcript challenges and all + // Halo2 verifier arithmetic. +diff --git a/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul +--- a/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul ++++ b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul +@@ -1,6 +1,10 @@ + // =============================================================== + // Lagrange & instance-evaluation block (pure Fr arithmetic). + // =============================================================== ++ // MF-4: hoisted so the section boundary below can tell a failed ++ // modexp (chain fault) from a rejected denominator (x landed on a ++ // domain point) instead of reporting both as PrecompileFailed. ++ let lagrange_precompile_failed := 0 + { + let k := {{ k }} + let x := mload(X_MPTR) +@@ -33,7 +37,7 @@ + } + let x_n_minus_1 := addmod(x_n, sub(r, 1), r) + mstore(mptr_end, x_n_minus_1) +- success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) ++ success, lagrange_precompile_failed := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + + // Convert inverted denominators into Lagrange evaluations: + // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). +@@ -88,4 +92,10 @@ + gas_checkpoint(11) // after Lagrange + instance evaluation block + {%- endif %} + +- if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } ++ if iszero(success) { ++ // A zero or non-canonical denominator is a rejected input, ++ // not a broken chain: the only way to reach it is a squeezed ++ // x that coincides with a domain point (probability ~n/r). ++ if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) } ++ fail(ERR_PROOF_REJECTED) ++ } +diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +--- a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol ++++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +@@ -10,7 +10,16 @@ + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). +- if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { revert(0, 0) } ++ // ++ // MF-2: typed like the runtime guard, and for a stronger reason. ++ // The runtime guard turns a bad recompile into a revert on every ++ // proof; this one turns it into a failed DEPLOYMENT, which is ++ // where a build fault belongs. The probes below keep bare reverts ++ // (a chain-capability failure, not a build fault). ++ if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { ++ mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED)) ++ revert(0x00, 0x04) ++ } + + // Scratch is reused for every runtime-prerequisite probe. + let scratch := {{ memory.constructor_smoke_scratch_mptr|hex() }} +diff --git a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul +--- a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul ++++ b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul +@@ -126,8 +126,14 @@ + // validate_public_accumulator returns a boolean to share the same + // success-plumbing style as other helper calls; this boundary is + // where the verifier converts failure to a revert. +- success := validate_public_accumulator(success, r) +- if iszero(success) { fail(ERR_BAD_POINT_ENCODING) } ++ let acc_precompile_failed := 0 ++ success, acc_precompile_failed := validate_public_accumulator(success, r) ++ if iszero(success) { ++ // MF-4: a G1MSM that could not run at all is a chain fault, ++ // not a malformed accumulator point. ++ if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) } ++ fail(ERR_BAD_POINT_ENCODING) ++ } + {%- endif %} + + {%- if self.gas_checkpoints %} +diff --git a/proofs/solidity-verifier/tests/template_digest.rs b/proofs/solidity-verifier/tests/template_digest.rs +--- a/proofs/solidity-verifier/tests/template_digest.rs ++++ b/proofs/solidity-verifier/tests/template_digest.rs +@@ -13,7 +13,7 @@ + + /// keccak over the sorted (path, length, content) stream of `templates/`. + const EXPECTED_TEMPLATE_TREE_DIGEST: &str = +- "0xfe89267c0778787678b938eb6f56f5e79f4bc11578786da45f2aad82384349d9"; ++ "0x063bb003b3f8eafacd300e1a20cb645db55832efa476689ddb83f6d781efb7dc"; + + fn collect_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("template directory is readable") { diff --git a/_to_delete/pr3.git.patch b/_to_delete/pr3.git.patch new file mode 100644 index 000000000..6407869ea --- /dev/null +++ b/_to_delete/pr3.git.patch @@ -0,0 +1,239 @@ +diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +--- a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md ++++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +@@ -1423,4 +1423,51 @@ + rest are mostly edge-case, interoperability, or validation issues, but several + could still become soundness bugs in a generated verifier. + ++ ++## 2026-08-13 — independent external review (MF series) ++ ++An independent review of the rendered `moonlight-wrap` artifacts (verifier ++sha256 `555ed976…6798`, VK `7ca78ec2…b7ec`; byte-identical to ++`fixtures/moonlight-wrap/`) run as three separate passes — cryptographic ++implementation, ZK/protocol soundness, and EVM/software security — plus a ++line-by-line equivalence pass against `midfall/proofs` and machine ++recomputation of every recomputable constant. ++ ++**No soundness-relevant defect was found.** The transcript schedule is ++byte-exact against the Rust verifier, every absorbed proof point provably ++reaches a subgroup-enforcing precompile, the linearization/PCS algebra is ++exact, and the memory plan has no live overlaps. Findings are one liveness ++bug and a set of hardening/diagnostic items. ++ ++| ID | Sev | Finding | Disposition | ++| --- | --- | --- | --- | ++| MF-1 | High | `MODEXP_GAS = 1360` is the EIP-2565 price; EIP-7883 (Osaka/Fusaka) prices the same frame at 4064, so every proof reverts `PrecompileFailed` on a repriced chain — and the constructor never probed modexp, so deployment succeeded silently | **Fixed** (`modexp_gas_word_frame` takes the max over live schedules; constructor modexp known-answer probe added) | ++| MF-2 | Low | The memory-layout guard — the only on-chain check for a bad recompile — reverted bare | **Fixed** (`MemoryLayoutViolated()`, typed on the runtime and constructor paths) | ++| MF-3 | Low | Quotient VM: fold-on-empty re-folds a stale top; native callbacks reset `q_sp` instead of asserting it, hiding dropped operands; u16 const indexes unclamped; `q_sp` unbounded | **Fixed** (four guards; u8 indexes keep their documented exemption, u16 do not) | ++| MF-4 | Low | `PrecompileFailed` / `BadPointEncoding` / `ProofRejected` conflated chain faults with rejected input on three paths | **Fixed** (cause flags threaded through `batch_invert` and `validate_public_accumulator`; `ec_pairing` split; triage table in `DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §6) | ++| MF-5 | Info | A low-level `staticcall` to an address with no code returns success — a mis-wired wrapper reads it as a valid proof | **Documented** (wrapper obligation W-4) | ++| MF-6 | Info | Single-reduction `mod r` sampling bias (max point mass ≈1.36× uniform) | **Accepted**, parity with the Rust reference; already covered by §5.1's ×1.3585 factor. Regenerate in lockstep if the reference moves to 512-bit reduction | ++| MF-7 | Info | 128-bit truncated challenges cap batching soundness | **Accepted**, already recorded and signed off as §5.1 (L-10) | ++| MF-8 | Info | Set-0 has 43 eval terms but 42 commitment terms (the committed-instance column's commitment is the identity), so its eval is forced ≈0 only indirectly via x1-batching | **Documented** in the PCS emitter | ++| MF-9 | Info | The canonical identity accumulator `(O, O)` is well-formed and passes the pairing layer | **Documented** (wrapper obligation W-5) | ++| MF-10 | Info | Native-identity selector folds appeared to hardcode a `y¹` gap | **WITHDRAWN — false positive.** All three emission sites already use `selector_fold.gap_for(identity)`; the fixed `Some(1)` is confined to `native_identity_estimate_block`, a size/gas proxy for gate-selection that is never emitted, and whose doc comment explicitly warns against "fixing" it to call `gap_for` (the fold plan is derived from the selection outcome, so it does not exist yet at that point). Recorded so the warning is not overridden by a future reviewer making the same mistake. | ++| MF-11 | Info | The transcript stream is positionally framed (a point and four scalars are byte-identical) | **Accepted**; identical to §5.2 (I-5). Any future variable-length section would need explicit length tags | ++| MF-12 | Info | `assembly ("memory-safe")` is unsound by the letter of the annotation | **Accepted**; safe here via the terminal block + FMP guard + pinned pragma, now noted at the annotation site | ++ ++Also proposed by the review and **withdrawn on inspection**: a CI job ++recomputing `vk_digest` from the rendered VK payload. The payload word is ++written directly from `self.vk.transcript_repr()` in `lowering/vk.rs` — a ++single expression over a single in-memory VK, not two independent paths — so ++such a test would assert `x == x`. The residual it was meant to close (that ++`vk_digest` binds the *semantic* constraint system) is not reachable this way; ++it is covered by the trace-replay tests and, off-transcript, by `BUILD_ID`. ++See §5.3 for the standing M-4 decision. ++ ++**Not closed by these commits:** the committed fixtures and the Sepolia ++deployment predate the MF-1 fix and are marked STALE; regeneration needs the ++pinned solc. The replay harness also cannot exercise EIP-7883 — the pinned ++revm 19 has an Osaka spec, but its modexp handler is still `berlin_run` — so ++real coverage awaits a revm bump; `src/evm.rs` records that gap at the ++`SpecId` pin. ++ + [1]: https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations" +diff --git a/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md +--- a/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md ++++ b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md +@@ -38,6 +38,24 @@ + root). Without this, any accepted proof can be replayed on every chain + and against every deployment of the same bytecode. + ++- **W-4 — Call it so a wrong address cannot read as success (MF-5).** A ++ low-level `verifier.staticcall(...)` returns `ok = true` with empty ++ returndata when the target has **no code** — a wrong address, a wrong ++ chain, or a wrapper configured before deployment reads as a valid proof. ++ Call through the typed interface (Solidity ≥0.8 inserts the `extcodesize` ++ check), or, on any low-level path, require `returndatasize() >= 32` AND a ++ decoded `true`. If the wrapper uses `try/catch`, every catch branch is a ++ rejection: the verifier's failures are custom errors, so `catch Error(string)` ++ and `catch Panic(uint)` will not match them — use `catch (bytes memory)` or a ++ bare `catch`. ++- **W-5 — Bind the accumulator's meaning, not just its validity (MF-9).** For ++ IVC renders, the verifier checks that the carried accumulator points decode ++ canonically, are in the subgroup, and satisfy the batched pairing equation. ++ It does NOT check that they are non-trivial or that they continue *your* ++ chain: the canonical identity encoding `(O, O)` is a well-formed accumulator ++ and passes by construction. Any "this accumulator continues the expected ++ fold" rule belongs to the circuit or the wrapper. ++ + `verifyProof`'s NatSpec states the same split: the raw verifier checks the + proof against the pinned VK and nothing else. + +@@ -80,11 +98,27 @@ + 6. **Retire** the old verifier in the deployment record (it cannot be + destroyed on-chain); wrappers must never point back at it. + +-**Upstream repricing note:** the verifier forwards exact EIP-2537/EIP-2565 +-scheduled gas. A fork that reprices those precompiles upward bricks +-`verifyProof` (liveness, not soundness) and deployment of new artifacts +-fails fast in the constructor probes. The migration path is the same +-re-render + wrapper switch. ++**On a fork of the chain you are deployed to — check this first (MF-1).** ++The verifier forwards exact scheduled gas to every precompile, so an upward ++repricing does not degrade: it bricks `verifyProof` outright (liveness, never ++soundness). The canonical symptom is a **sudden, total `PrecompileFailed` ++rate immediately after a fork activation**, on proofs that verified the day ++before and still verify against the native Rust verifier. Triage: ++ ++1. Compare the fork's precompile schedule against the deployed constants ++ (`G1ADD_GAS`, `G1MSM_GAS_*`, `PAIRING_GAS_2PAIR`, `MODEXP_GAS`). ++2. If any scheduled cost now exceeds the deployed bound, that is the cause; ++ re-render (the generator takes the maximum over live schedules) and ++ migrate per the steps above. There is no wrapper-side mitigation. ++ ++Worked example: EIP-7883 (Fusaka) removed the `/ 3` divisor from modexp ++pricing, taking the verifier's frame from 1360 to 4064 gas. Artifacts ++rendered before that fix carry `MODEXP_GAS = 1360` and revert every proof on ++any post-Fusaka chain — including the pre-fix `deployments/sepolia/` ++moonlight-wrap deployment, which should be assumed bricked and confirmed with ++a single `eth_call` before it is retired in the deployment record. Deployment ++of NEW artifacts now fails fast in the constructor probes for every ++precompile the runtime calls, modexp included. + + ## 5. Accepted risks (deployment owner sign-off) + +@@ -139,3 +173,30 @@ + (which does cover all of the above, off-transcript). Widening the digest is + a prover-affecting protocol change, explicitly out of scope by owner + decision (2026-08-13). ++ ++## 6. Reading a revert (MF-4) ++ ++`verifyProof` is success-or-revert: it returns `true` or reverts with one of ++the typed errors below. The taxonomy exists so the first question during an ++incident — *is this the chain, the build, or the proof?* — is answerable from ++the 4-byte selector alone, without a trace. ++ ++| Error | Selector | Class | First thing to check | ++| --- | --- | --- | --- | ++| `BadCalldataShape()` | `0x1b99e37c` | Caller | Heads, lengths, and EXACT `calldatasize`. A calldata-appending relayer (ERC-2771, multicall, paymaster) cannot call this contract directly. | ++| `VkMismatch()` | `0xa447d73e` | Deployment | The pinned VK address no longer has the expected runtime length/codehash, or a VK header word disagrees with the generated constants. | ++| `NonCanonicalScalar()` | `0x77530042` | Proof | A public instance or proof scalar is `>= r`. Usually an off-chain repacking bug, not an attack. | ++| `BadPointEncoding()` | `0xf27905ec` | Proof | A proof point violates the EIP-2537 padding/field bounds, or an accumulator public input failed canonical decoding. | ++| `PrecompileFailed()` | `0x84e81692` | **Chain** | A precompile could not run: missing, repriced above the forwarded bound (see §4), or short-returning. Also raised when G1MSM *rejects* a proof point as off-curve/out-of-subgroup — the precompile is the validator, so that rejection surfaces here by design. | ++| `ProofRejected()` | `0xc3b0d8cd` | Proof | The pairing ran and returned != 1, or a Lagrange denominator was zero (the squeezed `x` hit a domain point, probability ~n/r). | ++| `QuotientProgramInvalid()` | `0x3cc81b89` | **Build** | The VK-pinned quotient program violated a structural invariant. Not reachable with a well-formed artifact; treat as a generator bug. | ++| `MemoryLayoutViolated()` | `0xc9888d23` | **Build** | solc's stack-spill reservation overlaps the generated layout. The artifact was compiled off the pinned toolchain and can never verify anything; redeploy from the pinned `(version, --optimize-runs)` pair. | ++ ++Two rules of thumb: ++ ++- **Chain/Build classes are total, not probabilistic.** They fail every call, ++ including calls that verified yesterday. A sudden all-or-nothing failure ++ rate points here; a per-proof failure rate points at the Proof class. ++- **A revert is never an accept.** Every path above fails closed. There is no ++ configuration in which `verifyProof` returns `false` — see W-4 for why a ++ wrapper must not treat a bare `staticcall` success as verification. +diff --git a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md +--- a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md ++++ b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md +@@ -1574,9 +1574,14 @@ + The generated verifier constructor runs smoke tests: + + - `MCOPY` one-word round trip in constructor scratch. +-- `G1ADD(identity, identity) -> identity`. ++- `modexp(2, r-2, r) == 2^-1`, checked as `mulmod(result, 2, r) == 1`, at the ++ pinned `MODEXP_GAS` bound (MF-1). ++- `G1ADD(identity, identity) -> identity`, and the known answer `G1ADD(G, G) == 2G`. ++- `G1MSM([2]*G) == 2G`, and a **negative** probe: a point on the curve but ++ outside the r-order subgroup must be rejected. + - Largest generated `G1MSM` input with identity/zero terms -> identity. +-- `PAIRING_CHECK` over two identity `(G1, G2)` pairs -> true. ++- `PAIRING_CHECK` over two identity `(G1, G2)` pairs -> true, plus the known ++ answers `e(G,G2)e(-G,G2) == 1` and `e(G,G2)e(G,G2) != 1`. + + Deploy only on forks/chains where EIP-2537 and `MCOPY` are available with the + exact addresses, encodings, subgroup checks, return-size behavior, and enough +@@ -1591,9 +1596,15 @@ + - Exact return-data size. + - For pairing, returned word is 1. + +-EIP-2537 and modexp calls forward the exact EIP-2537/EIP-2565 scheduled cost +-(generated constants `G1ADD_GAS`, `G1MSM_GAS_*`, `PAIRING_GAS_2PAIR`, +-`MODEXP_GAS`; model in `src/lowering/layout/mod.rs::gas`), NOT `gas()`. A ++EIP-2537 and modexp calls forward exact scheduled costs (generated constants ++`G1ADD_GAS`, `G1MSM_GAS_*`, `PAIRING_GAS_2PAIR`, `MODEXP_GAS`; model in ++`src/lowering/layout/mod.rs::gas`), NOT `gas()`. For modexp the rendered bound ++is the MAXIMUM over the live schedules -- EIP-2565 prices the verifier's ++32/32/32 frame at 1360, EIP-7883 (Osaka/Fusaka) removes the `/ 3` divisor and ++prices it at 4080 -- because a bound below the chain's price does not degrade ++gracefully: the fixed-gas `staticcall` runs the precompile out of gas and every ++proof reverts `PrecompileFailed` (MF-1). Over-forwarding on a pre-Osaka chain ++costs nothing on success; unused gas is returned. A + rejecting precompile consumes everything forwarded to it, so exact bounds cap + what a malformed proof point can burn at the scheduled cost of the single + failing call instead of 63/64 of the transaction budget (M-2). The bounds are +diff --git a/proofs/solidity-verifier/src/lowering/kzg/mod.rs b/proofs/solidity-verifier/src/lowering/kzg/mod.rs +--- a/proofs/solidity-verifier/src/lowering/kzg/mod.rs ++++ b/proofs/solidity-verifier/src/lowering/kzg/mod.rs +@@ -109,6 +109,17 @@ + // as 128 zero bytes). Pin that justification here, where each query + // still knows its provenance: any other query source acquiring the + // identity pointer would make the emitters drop a real MSM term. ++ // ++ // MF-8: the visible consequence downstream is a set whose eval-term count ++ // exceeds its commitment-term count by one (e.g. `q_eval_set[0]: 43 ++ // evaluation term(s), 42 commitment term(s)` in the IVC render). That is ++ // not an off-by-one. The omitted commitment is the identity, so the ++ // multi-open equation still holds exactly -- and the omission is what ++ // FORCES the committed-instance eval to zero: the eval stays in the ++ // batched claim with coefficient trunc(x1^i) while contributing nothing ++ // to the commitment side, so any nonzero value breaks the opening. The ++ // enforcement is therefore indirect (via batching, per-term error ++ // 2^-128), not an equality check anywhere in the verifier. + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + for (source, query) in meta.protocol.pcs_queries.iter().zip(&queries) { + assert!( +diff --git a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +--- a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol ++++ b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +@@ -169,6 +169,16 @@ + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + // ++ // MF-12: by the letter of Solidity's memory-safety contract this ++ // annotation is a lie -- the block writes memory it never ++ // allocated through the free-memory pointer. Three properties ++ // make it safe HERE, and all three must hold together: this ++ // block is terminal (no Solidity executes after it), the pragma ++ // is pinned so codegen cannot shift underneath it, and the guard ++ // below fails closed if the spill reservation ever reaches the ++ // generated layout. Lifting this body into a non-terminal ++ // context, or unpinning the pragma, invalidates the annotation. ++ // + // MF-2: this is the only on-chain guard against a recompile that + // silently moves the spill region, and the failure it catches is + // permanent (no input can verify). `fail()` is not in scope this +diff --git a/proofs/solidity-verifier/tests/template_digest.rs b/proofs/solidity-verifier/tests/template_digest.rs +--- a/proofs/solidity-verifier/tests/template_digest.rs ++++ b/proofs/solidity-verifier/tests/template_digest.rs +@@ -13,7 +13,7 @@ + + /// keccak over the sorted (path, length, content) stream of `templates/`. + const EXPECTED_TEMPLATE_TREE_DIGEST: &str = +- "0x063bb003b3f8eafacd300e1a20cb645db55832efa476689ddb83f6d781efb7dc"; ++ "0x07b952304f2e76417023f215b63e13226ec63c14928e4cfee3312ee8fc3913f7"; + + fn collect_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("template directory is readable") { diff --git a/_to_delete/pr4.git.patch b/_to_delete/pr4.git.patch new file mode 100644 index 000000000..d4aa21dd1 --- /dev/null +++ b/_to_delete/pr4.git.patch @@ -0,0 +1,130 @@ +diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +--- a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md ++++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +@@ -1463,11 +1463,29 @@ + it is covered by the trace-replay tests and, off-transcript, by `BUILD_ID`. + See §5.3 for the standing M-4 decision. + ++**Verification.** The full EVM-gated suite runs green against these changes: ++217 passed / 0 failed under `--features evm,rust-verifier-trace`, including ++native-Rust-vs-Solidity trace equivalence, the constructor precompile-rejection ++tests, `typed_errors_identify_rejection_classes`, and ++`compiled_verifier_runtime_fits_the_eip170_limit` — so the added probe and ++guards do not breach the code-size limit. The MF-4 Lagrange re-route is ++observed executing rather than merely pinned in template text: the ++forced-domain-root verifier reverts with `ProofRejected()`. ++ ++Caveat on the compiler: the pinned NATIVE solc binary was not reachable from ++the environment that applied these fixes, so the suite ran against the same ++compiler commit built to WASM (npm `solc` 0.8.30, reporting ++`0.8.30+commit.73712a01`) behind a shim exposing the native CLI surface. That ++is sound for exercising behaviour, but this repository's reproducibility claim ++pins the native binary's sha256 — so nothing produced that way may be deployed ++or used to pin a hash, and the code-size result should be re-confirmed with the ++pinned binary before release. ++ + **Not closed by these commits:** the committed fixtures and the Sepolia +-deployment predate the MF-1 fix and are marked STALE; regeneration needs the +-pinned solc. The replay harness also cannot exercise EIP-7883 — the pinned +-revm 19 has an Osaka spec, but its modexp handler is still `berlin_run` — so +-real coverage awaits a revm bump; `src/evm.rs` records that gap at the +-`SpecId` pin. ++deployment predate the MF-1 fix and are marked STALE; regenerating them needs ++the SRS asset (unreachable here) and, for moonlight-wrap, a Moonlight checkout. ++The replay harness also cannot exercise EIP-7883 — the pinned revm 19 has an ++Osaka spec, but its modexp handler is still `berlin_run` — so real coverage ++awaits a revm bump; `src/evm.rs` records that gap at the `SpecId` pin. + + [1]: https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations" +diff --git a/proofs/solidity-verifier/fixtures/ivc/README.md b/proofs/solidity-verifier/fixtures/ivc/README.md +--- a/proofs/solidity-verifier/fixtures/ivc/README.md ++++ b/proofs/solidity-verifier/fixtures/ivc/README.md +@@ -7,8 +7,9 @@ + > inputs for the *replay* tests, which exercise verification logic rather than + > the modexp bound, but they must be regenerated (and their provenance rows + > below updated) on a host with the pinned solc before they are used as a +-> deployment source. Regeneration needs solc, which the environment that +-> applied the MF-1 fix did not have. ++> deployment source. Regeneration needs the SRS asset ++> (`zk_stdlib/examples/assets/bls_filecoin_2p19`) and a full proving run, which ++> the environment that applied the MF-1 fix could not reach. + + Pre-rendered artifacts for `tests/ivc_accumulator_replay.rs`, which replays a + real IVC final proof and then mutates the accumulator public inputs to check the +diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md +--- a/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md ++++ b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md +@@ -7,8 +7,9 @@ + > inputs for the *replay* tests, which exercise verification logic rather than + > the modexp bound, but they must be regenerated (and their provenance rows + > below updated) on a host with the pinned solc before they are used as a +-> deployment source. Regeneration needs solc, which the environment that +-> applied the MF-1 fix did not have. ++> deployment source. Regeneration needs a Moonlight checkout on the branch named ++> below plus the SRS asset, neither of which the environment that applied the ++> MF-1 fix could reach. + + Pre-rendered artifacts for the `point_pair` accumulator arm of + `tests/ivc_accumulator_replay.rs`. The IVC fixture next door covers +diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs +--- a/proofs/solidity-verifier/src/test.rs ++++ b/proofs/solidity-verifier/src/test.rs +@@ -1554,8 +1554,9 @@ + pragma solidity ^0.8.24; + + contract BatchInvertHarness {{ +- // The extracted helper forwards the exact EIP-2565 modexp cost; mirror +- // the generated constant it references. ++ // The extracted helper forwards the pinned modexp bound (the maximum over ++ // the EIP-2565 and EIP-7883 schedules, MF-1); mirror the generated ++ // constant it references. + uint256 internal constant MODEXP_GAS = {modexp_gas}; + + fallback() external {{ +@@ -1566,8 +1567,14 @@ + let r := calldataload(0x20) + let base := 0x1000 + calldatacopy(base, 0x40, mul(n, 0x20)) +- let ok := batch_invert(1, base, add(base, mul(n, 0x20)), 0x8000, r) ++ // MF-4: batch_invert now also reports WHY it failed (a failed ++ // modexp staticcall vs a rejected denominator). This harness only ++ // asserts fail-closed behaviour, so it keeps the boolean and ++ // discards the cause -- but it must still destructure both values ++ // or the extracted helper does not compile. ++ let ok, precompile_failed := batch_invert(1, base, add(base, mul(n, 0x20)), 0x8000, r) + mstore(0x80, ok) ++ pop(precompile_failed) + mcopy(0xa0, base, mul(n, 0x20)) + return(0x80, add(0x20, mul(n, 0x20))) + }} +@@ -2868,6 +2875,30 @@ + call_deployed_verifier(&mut evm, &fixture.proof, &fixture.instances), + "verifier with x forced to domain root should hit zero Lagrange denominator", + ); ++ ++ // MF-4: pin WHICH rejection this is, not just that it rejects. A zero ++ // Lagrange denominator means the squeezed x coincided with a domain point ++ // -- a transcript event, so ProofRejected. It used to surface as ++ // PrecompileFailed, which sends an incident responder to inspect the node ++ // when the proof was the cause. This is the only live path that reaches ++ // that branch, so without this assertion the split is pinned in template ++ // text but never observed executing. ++ let selector = { ++ use sha3::{Digest, Keccak256}; ++ let d = Keccak256::digest(b"ProofRejected()"); ++ vec![d[0], d[1], d[2], d[3]] ++ }; ++ match evm.evm.try_call_with_gas( ++ evm.verifier_address, ++ encode_calldata(&fixture.proof, &fixture.instances), ++ 30_000_000, ++ ) { ++ CallOutcome::Revert { output, .. } => assert_eq!( ++ output, selector, ++ "a zero Lagrange denominator must report ProofRejected, not a precompile fault" ++ ), ++ outcome => panic!("forced-domain-root verifier did not revert: {outcome:?}"), ++ } + } + + #[test] diff --git a/_to_delete/pr5.git.patch b/_to_delete/pr5.git.patch new file mode 100644 index 000000000..f56377a3d --- /dev/null +++ b/_to_delete/pr5.git.patch @@ -0,0 +1,100 @@ +diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +--- a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md ++++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +@@ -1464,7 +1464,7 @@ + See §5.3 for the standing M-4 decision. + + **Verification.** The full EVM-gated suite runs green against these changes: +-217 passed / 0 failed under `--features evm,rust-verifier-trace`, including ++218 passed / 0 failed under `--features evm,rust-verifier-trace`, including + native-Rust-vs-Solidity trace equivalence, the constructor precompile-rejection + tests, `typed_errors_identify_rejection_classes`, and + `compiled_verifier_runtime_fits_the_eip170_limit` — so the added probe and +@@ -1472,6 +1472,15 @@ + observed executing rather than merely pinned in template text: the + forced-domain-root verifier reverts with `ProofRejected()`. + ++MF-1's probe is likewise demonstrated rather than asserted. ++`constructor_rejects_a_modexp_bound_below_the_chain_price` renders the verifier, ++lowers `MODEXP_GAS` to one gas below what this harness's revm charges for the ++frame, and requires DEPLOYMENT to fail -- with a positive control deploying the ++same fixture unmutated, so the rejection cannot pass vacuously. That is the ++exact shape of the original bug (the shipped 1360 sat below EIP-7883's 4064), ++reproduced by moving the bound rather than the schedule, because the pinned ++revm 19 exposes `SpecId::OSAKA` but still prices modexp with `berlin_run`. ++ + Caveat on the compiler: the pinned NATIVE solc binary was not reachable from + the environment that applied these fixes, so the suite ran against the same + compiler commit built to WASM (npm `solc` 0.8.30, reporting +diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs +--- a/proofs/solidity-verifier/src/test.rs ++++ b/proofs/solidity-verifier/src/test.rs +@@ -1727,6 +1727,68 @@ + } + } + ++/// MF-1: the constructor's modexp probe must actually REJECT a bound below the ++/// chain's price, not merely be present in the rendered source. ++/// ++/// This is the property the whole MF-1 fix rests on. A `staticcall` forwards a ++/// fixed amount, so an under-priced `MODEXP_GAS` does not degrade -- the ++/// precompile runs out of gas and every proof reverts. Before the fix there was ++/// no modexp probe at all, so such an artifact DEPLOYED CLEANLY and only failed ++/// on first use; the probe exists to turn that into a failed deployment. ++/// ++/// The mutation lowers the bound to one gas below the EIP-2565 price this ++/// harness's revm charges (1354), which is exactly the shape of the real bug: ++/// the shipped 1360 sat below the EIP-7883 price of 4064. It cannot be tested ++/// by repricing revm instead -- the pinned revm 19 exposes `SpecId::OSAKA` but ++/// still prices modexp with `berlin_run` -- so the bound is moved rather than ++/// the schedule. Positive control: every other EVM test in this file deploys ++/// the same fixture unmutated. ++#[test] ++fn constructor_rejects_a_modexp_bound_below_the_chain_price() { ++ if !poseidon_inputs_available_for_evm() { ++ return; ++ } ++ ++ let rendered_bound = crate::lowering::layout::gas::modexp_gas_word_frame(); ++ let needle = format!("MODEXP_GAS = {rendered_bound};"); ++ // One gas below what revm's Berlin/EIP-2565 modexp charges for the ++ // verifier's 32/32/32 frame with a 255-bit exponent: max(200, 16*254/3). ++ let underpriced = (16 * 254 / 3) - 1; ++ ++ let fixture = create_property_poseidon_fixture(); ++ let verifier_solidity = { ++ assert!( ++ fixture.quotient_verifier_solidity.contains(&needle), ++ "rendered verifier should pin the generated modexp bound ({needle})" ++ ); ++ fixture.quotient_verifier_solidity.replacen( ++ &needle, ++ &format!("MODEXP_GAS = {underpriced};"), ++ 1, ++ ) ++ }; ++ ++ assert_pinned_quotient_constructor_rejects( ++ &verifier_solidity, ++ &fixture.vk_solidity, ++ &fixture.quotient_evaluator_solidity, ++ "modexp bound below the chain's price", ++ ); ++ ++ // Positive control, so the assertion above cannot pass vacuously: the same ++ // fixture, same compiler, same EVM, differing ONLY in that constant must ++ // construct successfully. Without this, a rejection caused by anything ++ // else in the pipeline would read as the probe working. ++ let mut evm = Evm::default(); ++ let vk_address = evm.create(compile_solidity(&fixture.vk_solidity)); ++ let quotient_address = evm.create(compile_solidity(&fixture.quotient_evaluator_solidity)); ++ evm.create_with_two_address_args( ++ compile_solidity(&fixture.quotient_verifier_solidity), ++ vk_address, ++ quotient_address, ++ ); ++} ++ + #[test] + fn production_renders_do_not_emit_gas_checkpoints() { + // The fixture's base variants follow `RenderDiagnostics::default()`, so a diff --git a/_to_delete/pr6.git.patch b/_to_delete/pr6.git.patch new file mode 100644 index 000000000..d0b72cfc4 --- /dev/null +++ b/_to_delete/pr6.git.patch @@ -0,0 +1,82 @@ +diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +--- a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md ++++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +@@ -1487,12 +1487,32 @@ + `0.8.30+commit.73712a01`) behind a shim exposing the native CLI surface. That + is sound for exercising behaviour, but this repository's reproducibility claim + pins the native binary's sha256 — so nothing produced that way may be deployed +-or used to pin a hash, and the code-size result should be re-confirmed with the +-pinned binary before release. ++or used to pin a hash. + +-**Not closed by these commits:** the committed fixtures and the Sepolia +-deployment predate the MF-1 fix and are marked STALE; regenerating them needs +-the SRS asset (unreachable here) and, for moonlight-wrap, a Moonlight checkout. ++The two builds were then compared directly, which upgrades that caveat from an ++assumption to a measurement: recompiling `deployments/sepolia/moonlight-wrap/ ++Halo2Verifier.sol` (a 21,161-byte via-IR runtime, at the `deployment.json` ++settings) through the WASM build reproduces the committed native-compiled ++runtime with exactly 40 differing bytes — the two 20-byte immutable ++`AUTHORIZED_VK` slots, which a fresh compile leaves zeroed. All 21,121 other ++bytes match. So the code-size result carries native weight; re-confirming it on ++a pinned-binary host remains good release hygiene rather than an open risk. ++ ++**Correction to an earlier claim in this section's history.** The MF-1 commit ++message and an earlier revision of `DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §4 ++stated that the `deployments/sepolia/moonlight-wrap` deployment should be ++assumed bricked post-Fusaka. That is wrong, and the assumption behind it was ++wrong: that deployment predates the exact-gas hardening, so all 17 of its ++`staticcall`s — the three modexp sites included — forward `gas()`, and an ++upward repricing is absorbed rather than fatal. Only artifacts carrying exact ++bounds are exposed, which is the population MF-1 addresses. Verified by ++reading the recorded source (its recompiled runtime reproduces ++`deployment.json`'s `runtimeCodeHash` byte-for-byte apart from the two ++immutable `AUTHORIZED_VK` slots), so no `eth_call` is needed to settle it. ++ ++**Not closed by these commits:** the committed fixtures predate the MF-1 fix ++and are marked STALE; regenerating them needs the SRS asset (unreachable here) ++and, for moonlight-wrap, a Moonlight checkout. + The replay harness also cannot exercise EIP-7883 — the pinned revm 19 has an + Osaka spec, but its modexp handler is still `berlin_run` — so real coverage + awaits a revm bump; `src/evm.rs` records that gap at the `SpecId` pin. +diff --git a/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md +--- a/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md ++++ b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md +@@ -111,14 +111,29 @@ + re-render (the generator takes the maximum over live schedules) and + migrate per the steps above. There is no wrapper-side mitigation. + +-Worked example: EIP-7883 (Fusaka) removed the `/ 3` divisor from modexp +-pricing, taking the verifier's frame from 1360 to 4064 gas. Artifacts +-rendered before that fix carry `MODEXP_GAS = 1360` and revert every proof on +-any post-Fusaka chain — including the pre-fix `deployments/sepolia/` +-moonlight-wrap deployment, which should be assumed bricked and confirmed with +-a single `eth_call` before it is retired in the deployment record. Deployment +-of NEW artifacts now fails fast in the constructor probes for every +-precompile the runtime calls, modexp included. ++Worked example: EIP-7883 (Fusaka, mainnet 2025-12-03, testnets earlier) ++removed the `/ 3` divisor from modexp pricing, taking the verifier's frame ++from 1360 to 4064 gas. An artifact rendered with exact bounds but before the ++MF-1 fix carries `MODEXP_GAS = 1360` and reverts every proof on any ++post-Fusaka chain. Deployment of NEW artifacts now fails fast in the ++constructor probes for every precompile the runtime calls, modexp included. ++ ++**The exposure is exactly the artifacts that carry exact bounds.** Note what ++that implies for `deployments/sepolia/moonlight-wrap`: it is NOT affected. It ++predates the exact-gas hardening entirely — all 17 of its `staticcall`s ++forward `gas()`, including its three modexp sites — so a repricing is simply ++absorbed from the caller's remaining gas. (Verified by reading the recorded ++source, whose recompiled runtime matches `runtimeCodeHash` in ++`deployment.json` byte-for-byte apart from the two immutable `AUTHORIZED_VK` ++slots, so the recorded source is genuinely what is deployed.) ++ ++That is the trade-off worth stating plainly, because it is easy to get ++backwards: **exact-gas forwarding is what creates repricing fragility.** The ++older `gas()`-forwarding renders survive any upward repricing but are exposed ++to the DoS that exact bounds were introduced to close (M-2) — a malformed ++proof point burns 63/64 of the transaction budget instead of one scheduled ++call. Neither property is free; the constructor probes exist so the fragility ++the current design accepts is caught at deployment rather than in production. + + ## 5. Accepted risks (deployment owner sign-off) + diff --git a/aggregation/CHANGELOG.md b/aggregation/CHANGELOG.md index 0c6bd7622..7183efcf3 100644 --- a/aggregation/CHANGELOG.md +++ b/aggregation/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://book.async.rs/overview * truncated_challenges feature to allow --all-features compilation [#146](https://github.com/midnightntwrk/midnight-zk/pull/146) * Rebase to new `circuits/` with `keccak` and `blake2b` [#135](https://github.com/midnightntwrk/midnight-zk/pull/135) ### Fixed +* Restore the transcript input bound required by `LightAggregator::verify`. * Fix cost model to pass correct number of committed instances [#280](https://github.com/midnightntwrk/midnight-zk/pull/280) ### Changed diff --git a/aggregation/src/light_aggregator/mod.rs b/aggregation/src/light_aggregator/mod.rs index d78d35022..3ef582708 100644 --- a/aggregation/src/light_aggregator/mod.rs +++ b/aggregation/src/light_aggregator/mod.rs @@ -81,7 +81,9 @@ use midnight_proofs::{ }, EvaluationDomain, }, - transcript::{CircuitTranscript, Hashable, Sampleable, Transcript}, + transcript::{ + CircuitTranscript, Hashable, Sampleable, Transcript, TranscriptHash, TranscriptInputBytes, + }, }; use rand::{CryptoRng, RngCore}; @@ -414,6 +416,11 @@ impl LightAggregator { C: Hashable, F: Sampleable + Hashable, u32: Hashable, + // Required by `plonk::prepare`, which this method delegates to. The + // bound is satisfied by both real transcript input types (`Vec` + // and `Vec`); propagating it here mirrors the other `prepare` + // callers in `proofs/tests/plonk_api.rs`. + ::Input: TranscriptInputBytes, { // Read the LHS of the acc from the transcript. let acc_lhs: Msm = { diff --git a/proofs/CHANGELOG.md b/proofs/CHANGELOG.md index 0037caefa..cc85f592a 100644 --- a/proofs/CHANGELOG.md +++ b/proofs/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Blind logup multiplicities polynomial on non-usable rows for ZK [#312](https://github.com/midnightntwrk/midnight-zk/pull/312) ### Changed +* Rework the Solidity verifier lowering pipeline and refresh its generated fixtures. * Simplify `CommitmentReference` by removing unused `Chopped` variant [#314](https://github.com/midnightntwrk/midnight-zk/pull/314) * Split linearization polynomial into non-constant and constant parts, removing the generator point from the MSM [#313](https://github.com/midnightntwrk/midnight-zk/pull/313) * Remove unnecessary polynomial padding in KZG multi-open [#276](https://github.com/midnightntwrk/midnight-zk/pull/276) diff --git a/proofs/solidity-verifier/.github/workflows/ci.yml b/proofs/solidity-verifier/.github/workflows/ci.yml deleted file mode 100644 index a41ec7a69..000000000 --- a/proofs/solidity-verifier/.github/workflows/ci.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - main - - "codex/**" - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - SOLC_INSTALL_DIR: ${{ github.workspace }}/.solc - SRS_DIR: ${{ github.workspace }}/.srs - -jobs: - default-rust-tests: - name: default Rust tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Install pinned solc - run: | - echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" - - run: cargo test --workspace --all-features --all-targets -- --nocapture - - real-evm-pbt-and-poseidon: - name: real EVM, PBT, Poseidon fixture - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Cache SRS assets - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: .srs - key: srs-poseidon-ivc-v1 - - name: Install pinned solc - run: | - echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" - - name: Ensure SRS assets - run: scripts/ensure_srs_assets.sh - - name: Real EVM PBT - run: | - HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ - cargo test --release --all-features pbt_ -- --nocapture - - name: Poseidon fixture - run: | - HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ - cargo test --release --features evm,truncated-challenges --test poseidon_fixture -- --nocapture - - poseidon-trace-equivalence: - name: Poseidon native/Solidity trace equivalence - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Cache SRS assets - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: .srs - key: srs-poseidon-ivc-v1 - - name: Install pinned solc - run: | - echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" - - name: Ensure SRS assets - run: scripts/ensure_srs_assets.sh - - name: Poseidon trace equivalence - run: | - HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ - cargo test --release \ - --features evm,truncated-challenges,rust-verifier-trace,solidity-trace \ - --lib native_midfall_verifier_trace_matches_solidity_trace \ - -- --nocapture - - full-ivc-bench: - name: full IVC bench and release bytecode sizes - runs-on: ubuntu-latest - timeout-minutes: 90 - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Cache SRS assets - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: .srs - key: srs-poseidon-ivc-v1 - - name: Install pinned solc - run: | - echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" - - name: Ensure SRS assets - run: scripts/ensure_srs_assets.sh - - name: Full IVC bench - run: scripts/run_ivc_bench.sh --skip-srs-download - - name: Release bytecode size/hash checks - run: scripts/check_release_bytecode_sizes.sh - - name: Upload IVC artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - if: always() - with: - name: ivc-keccak-solidity-dump - path: target/ivc-keccak-solidity-dump - - ivc-trace-equivalence: - name: IVC native/Solidity trace equivalence - runs-on: ubuntu-latest - timeout-minutes: 90 - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Cache SRS assets - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: .srs - key: srs-poseidon-ivc-v1 - - name: Install pinned solc - run: | - echo "SOLC=$(scripts/install_pinned_solc.sh "$SOLC_INSTALL_DIR" | tail -1)" >> "$GITHUB_ENV" - - name: Ensure SRS assets - run: scripts/ensure_srs_assets.sh - - name: IVC trace equivalence - run: scripts/run_ivc_bench.sh --trace --skip-srs-download - - lint: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: 1.90.0 - components: rustfmt, clippy - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 - - name: Rust format - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --workspace --all-features --all-targets -- -D warnings diff --git a/proofs/solidity-verifier/Cargo.toml b/proofs/solidity-verifier/Cargo.toml index bb7b24f07..c5b9a7674 100644 --- a/proofs/solidity-verifier/Cargo.toml +++ b/proofs/solidity-verifier/Cargo.toml @@ -42,9 +42,19 @@ midnight-circuits = { path = "../../circuits", features = ["testing"] } midnight-zk-stdlib = { path = "../../zk_stdlib", features = ["testing"] } # IVC Poseidon-chain final-step Keccak proof end-to-end test # (tests/ivc_keccak_solidity.rs). +# +# Deliberately does NOT enable midnight-aggregation/truncated-challenges +# here: a dependency-level feature is unconditional, and via feature +# unification it forced midnight-proofs/truncated-challenges into EVERY +# build of this crate — including `--features evm` builds whose generated +# verifiers do not mirror the truncation. The prover then truncated while +# the rendered Solidity did not, and every valid proof from the +# `evm`-only fixture tests reverted mid-PCS ("using the wrong setting on +# either side silently produces invalid pairings", as the feature comment +# below puts it). Truncation is forwarded exclusively through this +# crate's own `truncated-challenges` feature so both sides always agree. midnight-aggregation = { path = "../../aggregation", features = [ "keccak-transcript", - "truncated-challenges", ] } [features] diff --git a/proofs/solidity-verifier/build.rs b/proofs/solidity-verifier/build.rs new file mode 100644 index 000000000..9df918e7c --- /dev/null +++ b/proofs/solidity-verifier/build.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Exposes the crate's enabled feature profile to the generator (P10/L-8, +//! docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). +//! +//! The rendered verifier's proof schema depends on feature flags +//! (`truncated-challenges`, `fewer-point-sets`, ...), and nothing in the +//! deployed artifact previously recorded which profile produced it. The +//! generator folds this string into the emitted `BUILD_ID` constant. + +fn main() { + let mut features: Vec = std::env::vars() + .filter_map(|(key, _)| { + key.strip_prefix("CARGO_FEATURE_") + .map(|name| name.to_ascii_lowercase().replace('_', "-")) + }) + .collect(); + features.sort(); + println!( + "cargo:rustc-env=SOLIDITY_VERIFIER_FEATURES={}", + features.join(",") + ); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md index 0a2c41b79..130a695cf 100644 --- a/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md +++ b/proofs/solidity-verifier/deployments/sepolia/moonlight-wrap/README.md @@ -4,6 +4,64 @@ This directory contains the generated Moonlight wrap verifier contracts that were deployed to Sepolia, plus the exact runtime bytecode fetched back from the chain. +## Status: deployed code predates current codegen + +**The contracts at the addresses below are still the ones described here, but +the generator has since moved on. They are not reproducible from current +`main`.** Nothing in this directory has been regenerated -- the sources and +bytecode remain exactly what was deployed, because they are the record of what +is on chain. + +Re-rendering the same circuit at `e5300d4` produces a different verifier: + +| | Deployed | Current codegen | +| --- | ---: | ---: | +| `Halo2Verifier.sol` | 206,619 bytes | 212,419 bytes | +| Verifier runtime | 21,161 bytes | 21,203 bytes | +| `TRANSCRIPT_MPTR` | `0x80` | `0x1000` | + +The verifying key also differs, in its `quotient_program` section; the circuit +itself is unchanged (`acc_offset = 11`, 19 public inputs, `point_pair`). + +Fixes made after this deployment that it therefore does **not** carry: + +- **Memory layout rebase.** The deployed verifier bases its layout at `0x80`, + inside the `[0x80, 0x8e0)` window solc reserves for via-IR stack-to-memory + spill slots -- see AUDIT.md TA-5. It has not misbehaved, but the separation + rests on spill placement rather than on anything enforced. +- **Accumulator identity guard.** `load_acc_coord_shifted` used a bitwise `and` + against a radix base, making the guard false on every call; the identity + branch was dead and the canonicality barrier ineffective. Fail-closed, not a + forgery path. +- **Pairing result check.** `ec_pairing` folded the precompile result with a + bitwise `and`, accepting any odd return word rather than exactly `1`. +- **Constructor smoke test.** Probes used identity-only EIP-2537 vectors, which + a non-conformant precompile can satisfy without doing curve arithmetic. + +Redeploying is a deliberate on-chain action requiring a funded keystore and an +RPC endpoint; see "Recreate The Deployment" below. Until then this record stays +as-is and accurate. + +## Verifying the deployed bytecode + +The tracked source reproduces the on-chain runtime exactly, apart from the +immutable address slots that are substituted at deployment: + +```bash +solc --bin-runtime --optimize --optimize-runs 200 --via-ir \ + --evm-version cancun --no-cbor-metadata Halo2Verifier.sol +``` + +That yields 21,161 bytes -- matching `runtimeBytes` -- with codehash +`0x79432a36a98570db8c04b9c5cc23994477089eabd2501cfe0b0a78ae5f3c38f8`. + +It does **not** equal the recorded `runtimeCodeHash`, and should not: comparing +compiled output byte-for-byte against on-chain code shows exactly 40 differing +bytes, in two 20-byte runs at offsets `0x51` and `0x125`. Those are the two +placeholder slots for `address public immutable AUTHORIZED_VK`, filled in by the +constructor. Every other byte is identical. Verified at `e5300d4` with +`solc 0.8.30+commit.73712a01`. + ## Addresses | Contract | Address | diff --git a/proofs/solidity-verifier/docs/architecture/ARCHITECTURE_REVIEW_2026-08.md b/proofs/solidity-verifier/docs/architecture/ARCHITECTURE_REVIEW_2026-08.md new file mode 100644 index 000000000..b47eac771 --- /dev/null +++ b/proofs/solidity-verifier/docs/architecture/ARCHITECTURE_REVIEW_2026-08.md @@ -0,0 +1,466 @@ +# Halo2 Solidity Verifier Code Generator — Architecture Review + +> Independent as-built architecture review, produced 2026-08-08 against the +> `misc-fixes` branch. This document describes the system as it exists in the +> source tree, from a full multi-reviewer pass over every module; it is a +> snapshot, not a maintained spec. The maintained specification is +> [`LOWERING_ARCHITECTURE_SPEC.md`](./LOWERING_ARCHITECTURE_SPEC.md); +> design-choice notes are in [`ARCHITECTURE.md`](./ARCHITECTURE.md). The +> companion assessment and redesign proposals are in +> [`../plans/REDESIGN_PROPOSALS_2026-08.md`](../plans/REDESIGN_PROPOSALS_2026-08.md). +> Reported line numbers and counts are as of this snapshot; verify against +> current sources before relying on them. + +## 1. What this crate is + +`halo2_solidity_verifier` is a **code generator**, not a verifier library. It +consumes a `midnight-proofs` verifying key plus BLS12-381 KZG parameters and +emits up to three Solidity contracts: + +| Artifact | Role | +| --- | --- | +| `Halo2Verifier.sol` | The verifier: one `verifyProof(bytes,uint256[])` entry point wrapping a single terminal Yul assembly block. | +| `Halo2VerifyingKey.sol` | Optional split VK data contract: runtime bytecode = `0xfe` (INVALID) prefix + the raw VK payload (header words, compact quotient VM constants + bytecode, fixed/permutation commitments). | +| `Halo2QuotientEvaluator.sol` | Optional split quotient evaluator: a `fallback()` that receives a raw verifier memory frame by calldata, re-runs the quotient VM, and returns `[magic, -nu_y(x), selector buckets]`. | + +It also provides the off-chain per-proof shim: `repack_proof` / +`encode_calldata` convert a native compressed proof (48-byte G1s, LE scalars) +into the EIP-2537-padded big-endian calldata the generated verifier reads. + +The supported protocol envelope is deliberately narrow and validated up +front: Midfall/Midnight KZG proofs on BLS12-381, exactly one identity-committed +instance column plus one non-committed public-input column, no rotated instance +queries, at least one advice column, Keccak transcript, optional IVC +accumulator tail in radix-2^56 limbs (7 limbs × 56 bits, 4 limbs per word). +The generator emits exactly **one verifier shape** — there is no runtime +configurability in the artifact; every knob is resolved at generation time. + +Scale at this snapshot: ~35k lines of Rust (of which ~8.6k are tests inside +`src/`), ~3.8k lines of Askama templates, 18 template files, 37 documentation +files, 2 libFuzzer targets, 7 integration test files, and committed replay +fixtures for IVC and Moonlight-wrap shapes. + +## 2. System at a glance + +```mermaid +flowchart TD + subgraph public["builder/ + api.rs + evm.rs (public facade)"] + A["SolidityGenerator::try_new\n(shape validation)"] --> B["VerifierBuildInputs\n(read-only snapshot)"] + end + subgraph lowering["lowering/ (private pipeline)"] + B --> C["ProtocolPlan\n(protocol/)"] + C --> D["ProofCalldataLayout +\nTranscriptBufferLayout (abi/)"] + C --> E["Quotient identity stream\n(quotient.rs + yul_emit.rs)"] + E --> F["Compact VM program\n(vm/mod.rs)"] + F --> G["VK payload w/ embedded\nprogram (vk.rs, layout/vk_payload.rs)"] + G --> H["VerifierMemoryLayout\n(layout/memory.rs)"] + C --> I["PCS query plan + Yul blocks\n(kzg/)"] + D & F & G & H & I --> J["LoweringPlan (plan.rs)\nconverged + invariant-checked\n+ certified"] + end + subgraph render["render models + Askama templates"] + J --> K["Halo2Verifier /\nHalo2VerifyingKey /\nHalo2QuotientEvaluator models\n(render/models.rs, artifacts.rs)"] + K --> L["templates/*.sol + *.yul\n→ Solidity source"] + end + subgraph perproof["per proof (off-chain)"] + J --> M["RepackedProofLayoutPlan\n(calldata.rs)"] + M --> N["verifyProof calldata"] + end +``` + +Two properties define the architecture: + +1. **Layout-first, plan-once.** The generated Yul uses absolute memory + addresses and packed bytecode sections, so *every* address, offset, count, + and byte length is computed and validated in Rust before a single template + line renders. One converged `LoweringPlan` per `render()` call feeds the + verifier, the VK, and the quotient evaluator, so one call can never emit + artifacts from divergent plans (`src/builder/render.rs:34`). +2. **Templates consume facts.** Askama templates receive already-planned data + (pointers, counts, code-line vectors, opcode tables) and are forbidden by + convention from re-deriving layout. Every template constant traces to a + single Rust definition via `TemplateConstants` / `VkHeaderTemplateSlots`. + +## 3. Layering and dependency rules + +``` +src/lib.rs public exports, feature constants +src/api.rs public config/error/diagnostic types (no codegen logic) +src/builder/ thin validating facade: SolidityGenerator + api.rs try_new + shape validation, diagnostics entry points + render.rs render(RenderOptions) → RenderedArtifacts + repack.rs repack_proof / encode_calldata wrappers +src/lowering/ private pipeline (all pub(crate)) + protocol/ typed ProtocolPlan from the constraint system + abi/ proof calldata + transcript buffer layout + encoding/ ConstraintSystemMeta, Data, Ptr/Word/EcPoint handles, + EIP-2537 encoders + layout/ memory planner, VK header/payload section maps + quotient.rs quotient identity planning + routing + selector folds + quotient_numerator/ yul_emit.rs (identity Yul emitter), + vm/ (compact VM compiler, validators, reference + interpreter, certifier) + kzg/ PCS multi-open planner and Yul emitter + plan.rs LoweringPlan: convergence + cross-module invariants + vk.rs VK payload generation, convergence loops + artifacts.rs Askama model assembly + render/ models.rs (dumb template carriers + validators) + calldata.rs proof repacking + diagnostics.rs host-side manifests/counts +src/evm.rs calldata ABI encoder; pinned-solc + revm harness (evm feature) +templates/ Askama contract templates + partials +``` + +Dependency rules that hold in the imports (verified, not just documented): + +- `builder` → `lowering` only; **zero** reverse dependencies from `lowering` + into `builder`. +- All pipeline entry points are methods on `VerifierBuildInputs`, a `Copy`, + read-only snapshot of the generator's borrowed state + (`src/lowering/mod.rs:36`). +- `render/models.rs` structs are dumb carriers; planning types do not depend + on Askama. +- Templates reach Rust constants only through model fields; opcode/token bytes + come from `QUOTIENT_VM_SPEC` (`src/lowering/quotient_numerator/vm/mod.rs:908`), + the single ABI description shared by emitter, validators, tests, and + template rendering. + +## 4. The lowering pipeline, stage by stage + +### 4.1 Constructor validation (`builder/api.rs`) + +`SolidityGenerator::try_new` rejects, with typed `GeneratorError`s: missing +advice columns, any instance-column split other than 1 committed + 1 +non-committed, invalid accumulator limb schema (only 7×56 supported), +accumulator fixed-base scalar tails outside +`[1 + num_permutation_comms, 1 + num_permutation_comms + num_fixed_comms]` +(which would otherwise alias arbitrary VK payload words as G1 bases), +rotated instance queries, and any constraint-system shape +`ProtocolPlan::validate` refuses (~15 cross-field invariants, e.g. an advice +column absorbed but never opened). `new()` is the panicking wrapper. + +### 4.2 Protocol planning (`protocol/`) + +`ProtocolPlan::try_from_constraint_system` derives the typed source of truth: +commitment/eval read order in transcript order, the PCS query schedule (ending +in a synthetic Linearization query), per-family quotient identity counts, +phase remappings, the simple-selector column set, common-poly needs, and trace +IDs. Everything downstream — proof ABI, transcript sizing, quotient stream, +PCS sets — is derived from this plan, and `ConstraintSystemMeta` carries its +scalar summary with a `validate_against_protocol` cross-check. + +### 4.3 Proof ABI (`abi/proof.rs`) + +`ProofCalldataLayout` replays the protocol commitment order into byte offsets +(per-phase advice, lookup m/helpers/accumulators, permutation products, trash, +quotient limbs, evals, `f_com`, q_eval sets, public inputs), asserting order +drift. `TranscriptBufferLayout` conservatively sizes the streaming Keccak +buffer (largest absorb run between squeezes, at 128 bytes per uncompressed G1) +— this bound decides `VK_MPTR`, and an under-estimate would let the transcript +buffer overrun the VK region mid-verify (the comment at +`src/lowering/vk.rs:437` records exactly this historical bug class). + +### 4.4 Quotient identity planning (`quotient.rs`, `yul_emit.rs`) + +The `yul_emit::Evaluator` walks gates, permutation, lookup (LogUp), and trash +arguments in the canonical `partially_evaluate_identities` order, emitting +per-identity Yul lines **and** a typed `QuotientExpr` tree per identity, each +tagged `Main` (folds into the numerator accumulator) or `Selector(i)` (folds +into a simple-selector bucket). `quotient_program_plan` then routes each +identity to one of four execution representations: + +| Representation | What | Why | +| --- | --- | --- | +| Inline Yul prefix | First `DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES = 4` gates | Straight-line speed for the cheap head | +| Compact VM bytecode | Bulk of the identities | Code size: bytecode lives in the VK payload, not the verifier runtime | +| Native Yul callbacks | Knapsack-selected heavy gates (`DEFAULT_QUOTIENT_NATIVE_GATES = 4` budget) + the whole permutation/lookup families | Gas: loops with scratch tables beat interpretation for heavy shapes | +| Structured trash tail | Trash identities | Compressed fixed-shape tail | + +`validate_execution_manifest` re-expands the routed plan back to a flat +per-identity stream and proves position-by-position equality of index, source, +and target against the original — the routing is provably order-preserving. +`selector_fold_plan` computes per-bucket y-power gaps/tails so selector buckets +fold sparsely. Cost models used by the knapsack are explicitly documented as +proxies whose divergence cannot affect correctness. + +### 4.5 Compact quotient VM compilation (`quotient_numerator/vm/`) + +`QuotientProgramBuilder` lowers `QuotientExpr` trees into a byte-oriented +program plus a deduplicated Fr constant table: constant pooling (u16 slots with +a u8 fast path), stack-depth accounting, peepholes (pow5, fused add-mul +accumulators), and **structural** shape recognizers that rewrite 7-limb +foreign-field algebra (LIN7 / BILIN7 / MODARITH7 shapes) and affine sums into +superinstructions; a run-compaction pass turns ≥4 adjacent fused terms into +counted `RUN_*` opcodes. Recognizers never dispatch on gate names and always +have a value-equal generic fallback. + +The finalized bytecode passes a three-stage offline validator +(decode/operand-bounds/stack-effects; const-slot bounds; memory-pointer +whitelist against a `QuotientReadModel` of exactly the windows the verifier +populates), then two certification passes (§8). + +### 4.6 VK payload and convergence (`vk.rs`, `layout/vk_payload.rs`) + +The VK payload is a monotonic section map: 31-word header (digest, domain +constants, accumulator config, EIP-2537 G1/G2/−sG2 base points) built by a +checked `VkHeaderBuilder`, quotient constants, packed quotient program +(big-endian U256 words, padding verified on decode), fixed commitments, +permutation commitments. + +Two bounded fixed-point loops resolve circular size dependencies, failing +loudly on non-convergence: + +- `generate_vk` (≤8 iterations): program size depends on memory addresses, + which depend on VK length — reserve zero-filled sections, recompile, repeat + until the reservation covers the program. +- `meta_data_for_stable_static_layout` (≤3 iterations): `VK_MPTR` must sit + above the low-memory working set (transcript buffer, PCS scratch, pairing + frames, return buffers), which itself depends on planned metadata (including + the two-pass dummy-eval/point-set planning for `outer-fewer-point-sets`). + +`generate_base_vk` also asserts at build time that the SRS G1 base (sum of +`g_lagrange`) equals the canonical generator — otherwise the emitted `G1_BASE` +would diverge from the commitment base and the contract would enforce a +different pairing equation than the native verifier (`src/lowering/vk.rs:74-92`). + +### 4.7 Memory planning (`layout/memory.rs`) + +Because the generated Yul never uses the free-memory pointer for its main work +areas, `VerifierMemoryLayout::new` registers ~30 named regions in a +`MemoryArena`: fixed low-memory frames at `LOW_MEMORY_SCRATCH_START = 0x1000` +(transcript buffer, return words, pairing scratch, constructor smoke), +permanent bands (VK payload, challenges, theta window, decoded evals, +decompressed commitments), and phase-scoped scratch (batch inversion, quotient +VM stack, PCS tables/MSMs, accumulator MSM). Each region carries a lifetime: +`Permanent`, `Phase(p)`, or `PhaseSpan(a..=b)` over a 16-variant `MemoryPhase` +enum declared in verifier runtime order — the derived `Ord` is what makes +`MemoryLifetime::intersects` meaningful. `validate()` pins fixed pointers, +rejects regions below 0x1000, checks PCS window capacities (rotation points +≤ 28, x1 powers ≤ 65, q_eval sets ≤ 56), and runs pairwise overlap checking +gated by lifetime intersection — so intentional scratch aliasing across +disjoint phases is declared and machine-checked rather than coincidental. + +The 0x80–0x1000 band is deliberately left to solc: the terminal assembly block +carries a *factually false* `("memory-safe")` annotation (required to compile +without stack-too-deep), and disjointness from solc's via-IR spill window is +established by testing the compiled bytecode's memoryguard against the layout +(observed spill reservations up to 0x8e0; see §8 and the companion assessment). + +### 4.8 PCS planning and emission (`kzg/`) + +The KZG multi-open lowering mirrors `midnight-proofs` `multi_prepare`: +`queries()` resolves the typed plan sources to concrete memory handles; +optional dummy-query augmentation collapses rotation sets +(`outer-fewer-point-sets`); `construct_intermediate_sets_impl` simulates the +prover's set construction (dedup by pointer identity, cardinality-sorted); +`memory_requirements` sizes the scratch windows; `computations()` emits six Yul +blocks — rotation points `x·ω^rot`, x1 powers (rolled loop, optional 128-bit +truncation), per-set q_eval folds (rolled when >4 sets), `f_eval` via Horner +with a single-modexp Montgomery batch inversion, the fused final G1MSM through +precompile 0x0c, and pairing inputs `(π, final_com − v·G + x3·π)`. + +`validate_absorbed_g1_precompile_coverage` machine-checks the crate's central +soundness delegation: **every** proof G1 absorbed into Fiat-Shamir must later +be consumed by an EIP-2537 G1MSM or pairing input (which subgroup-check), +because the verifier itself never subgroup-checks; `G1ADD` (which does not) is +only ever applied to precompile outputs. + +### 4.9 Render models and templates (`render/models.rs`, `artifacts.rs`, `templates/`) + +`artifacts.rs` assembles the three Askama models from the converged plan and +calls their `validate_layout()` / `validate_payload_layout()` before rendering +(pointer/count cross-checks against the typed layouts, external-frame +containment, EIP-170 bound on the VK runtime). Templates are organized as one +contract shell per artifact plus partials in phase order: precompile smoke, +constructors (2×2: embedded/split VK × inline/external quotient, pinning +dependencies by `code.length` **and** `codehash`), VK loading + full ABI +calldata-shape validation, transcript parser, Lagrange block, quotient block +(dual-mode: inline VM or external staticcall over a raw frame), PCS injection, +accumulator batching, final pairing, trace/return. + +### 4.10 Per-proof repacking (`calldata.rs`, `evm.rs`) + +`repack_proof` decompresses each 48-byte G1 (rejecting invalid points), +pads to the 128-byte EIP-2537 form, rewrites LE scalars into canonical BE +words (rejecting non-canonical Fr with typed `RepackError`s carrying byte +offsets and hex payloads), preserving exactly the generator's group ordering; +`evm::encode_calldata` wraps the result in the `verifyProof(bytes,uint256[])` +ABI (selector `0x1e8e1e13`). + +## 5. Design decisions and their rationale + +1. **Narrow supported envelope, validated up front.** The generator refuses + at construction what the templates cannot verify at runtime. This converts + a whole class of "generated verifier silently wrong" into "generation + fails with a typed error." +2. **Static absolute memory layout.** Predictable, cheap Yul (no FMP + bookkeeping), auditable addresses, and a planner that can *prove* + non-overlap. The cost: the planner is load-bearing for memory safety, and + the 0x80–0x1000 solc band must be established out-of-band (§4.7). +3. **Compact quotient VM in the pinned VK payload.** The quotient block was + the dominant contract-size contributor (straight-line Yul put the IVC + verifier over EIP-170). Moving identity programs into VK data and + interpreting them trades gas for size; macro/run opcodes and limb + superinstructions claw the gas back (quotient section: 2.58M → 1.05M gas; + verifier runtime: 30,278 → ~12k bytes, each artifact below 24KB). +4. **Pinning by runtime length + codehash, both at construction and per + call.** The verifier accepts exactly one VK runtime hash and (in split + mode) one quotient evaluator hash. Deployment is a two-phase flow (render + evaluator → compile/deploy → re-render verifier with `ExternalPinned`); + the older unpinned APIs deliberately panic. +5. **Bounded convergence instead of two-pass guesswork.** Circular size + dependencies are resolved by explicit fixed-point loops that fail loudly, + and `LoweringPlan::new` re-checks the cross-module invariants convergence + was supposed to establish — including a *word-for-word* comparison of the + VK-embedded quotient constants/bytecode against an independently + recompiled build (`src/lowering/plan.rs:307-329`). +6. **Render-time certification of emitted bytecode** (§8). Optimizers are + checked per artifact, not trusted: a recognizer bug on an unseen gate + shape fails the render instead of shipping a wrong verifier. +7. **Subgroup checks delegated to EIP-2537 under a machine-checked coverage + invariant** (§4.8), with a constructor smoke test that fails deployment on + forks without the precompiles. +8. **One verifier shape per build.** Feature flags (`truncated-challenges`, + `outer-fewer-point-sets`, `outer-single-h-commitment`) select codegen + behavior at compile time; the artifact has no runtime configuration + surface. (The flip side — the artifact records its feature profile + nowhere — is assessed in the companion document.) + +## 6. The generated runtime + +Execution order of a rendered verifier (each phase maps to a template partial +and a `MemoryPhase` variant): + +1. ABI guard: proof head must be exactly `0x40`, instances head pinned, proof + length, instance count, and **exact** `calldatasize` checked before the + parser touches a byte. +2. VK load: embedded `mstore`s, or `extcodesize`/`extcodehash` re-check + + `extcodecopy` from byte 1 of the pinned VK contract; loaded header words + cross-checked against generated constants (`num_instances`, `k`, + accumulator config). +3. Optional accumulator pre-validation: radix-2^56 limb decode (injective; + canonical identity encoding; packing canonicality) with every decoded point + routed through G1MSM for curve/subgroup validation. +4. Streaming Keccak transcript: absorb VK digest, identity committed-PI + (128 zero bytes), instance count + canonicality-checked instance scalars, + then per-phase advice G1s (128-byte padded form) interleaved with + challenge squeezes (theta, beta/gamma, trash, y, x, x1..x4; optional + 128-bit truncation); evals spill to `REVERSED_EVALS_MPTR`; final + `proof_cptr == NUM_INSTANCE_CPTR` consumption check. +5. Lagrange block: `x^n` by squaring, denominator batch-inversion + (fail-closed on non-canonical words / failed modexp), `L_i(x)`, + `instance_eval`. +6. Quotient numerator: interpret the pinned VM bytecode (cached-top-of-stack + interpreter; only the opcode cases the program uses are rendered; unknown + opcodes revert; terminal `q_pc`/`q_has_top`/`q_sp` balance checks) plus + native permutation/lookup/heavy-gate kernels; or staticcall the pinned + external evaluator with a raw memory frame, checking returndata size and a + magic word. Result: `-nu_y(x)` at `QUOTIENT_EVAL_MPTR`. +7. PCS blocks 1–6 (§4.8) ending in `PAIRING_LHS`/`PAIRING_RHS`. +8. Optional accumulator batching: Keccak-derived alpha (zero remapped to 1) + folds the carried accumulator equation into the pairing inputs via + G1MSM + G1ADD. +9. Two-pair pairing via `ec_pairing` (MCOPY-staged, result compared + `eq(..., 1)`); `mstore(RETURN_MPTR, 1)`; return. + +Every staticcall checks success **and** exact returndatasize; every scalar +ingress is canonical-Fr-checked; every G1 coordinate checks the 16-byte pad +and `coord < p`. Failure direction is uniformly revert. + +## 7. Trust and assurance architecture + +The correctness argument is a chain with explicitly different assurance per +leg (honestly documented in `LOWERING_ARCHITECTURE_SPEC.md` §12.1 and +`vm/reference.rs`): + +| Leg | Mechanism | Strength | +| --- | --- | --- | +| Identity expressions → VM bytecode | Render-time: 3-stage validator; `certify_quotient_program` executes finalized bytecode on an **independent reference interpreter** at an artifact-seeded challenge vs direct `QuotientExpr` evaluation; `certify_quotient_builds_agree` dual-builds with recognizers disabled and requires identity-by-identity agreement; VK-embedded words re-compared word-for-word | Per-render, machine-checked | +| Bytecode → Yul interpreter semantics | Opcode/token table conformance tests (a `case` exists per opcode) + native-vs-Solidity per-identity trace differentials on fixture circuits | Fixture-sampled; an opcode no fixture emits has unverified runtime semantics | +| Inline/native/tail identities (never bytecode) | Trace differentials only | Fixture-sampled | +| Pointer bindings wrong-in-both-representations | `validate_quotient_mem_ptrs` whitelist (certification can't catch these: a pointer wrong in both sides agrees with itself) | Per-render | +| Memory non-overlap | Lifetime-aware `MemoryMap::validate` per render; *registered-regions-only* — agreement between emitted Yul and registered regions is pinned by rendered-source tests | Per-render + tests | +| solc spill window vs 0x1000 layout | Compiled-bytecode memoryguard test (env-gated) | Test-time only, one pinned compiler | +| EIP-2537 semantics (incl. subgroup checks) | Assumed per spec; constructor smoke proves existence/arithmetic, not rejection behavior | Assumed | +| Transcript equivalence with native verifier | Byte-for-byte mirror + trace differential + real-proof fixtures | Fixture-sampled | + +Complementing this, the adversarial test tiers: proof/VK/calldata mutation +PBTs, per-offset G1 canonicality/off-curve rejection sweeps cross-checked +against the native verifier, batch-invert fail-closed harness on extracted +generated Yul, EIP-170 fit, a 7-case supported-shape fuzz matrix with +cross-wiring rejection, SRS-free replay fixtures with gas-based stage +attribution, and two libFuzzer targets. (Which of these actually run in CI is +a finding in the companion assessment.) + +## 8. Cross-cutting contracts + +Things that must stay in sync, and how they currently are: + +| Contract | Mechanism | +| --- | --- | +| Quotient VM opcode/token bytes ↔ Yul interpreter | One `QUOTIENT_VM_SPEC` constant feeds emitter, validators, template constants, conformance tests | +| VK payload section order ↔ runtime `extcodecopy` offsets | Typed `VkPayloadLayout`, validated contiguity, tests | +| Proof read order ↔ transcript absorbs ↔ repacker | All derived from `ProtocolPlan` / `ProofCalldataLayout`; drift asserted | +| `MemoryPhase` enum order ↔ template include order | **Comment-enforced only** (see assessment) | +| Injected Yul code lines ↔ template-scope identifiers | **Naming convention only** (see assessment) | +| `verifyProof` selector + ABI head layout ↔ templates | Constant + duplicated in tests/fuzz targets | +| Feature flags ↔ expected proof schema | **Recorded nowhere in the artifact** (see assessment) | +| Accumulator limb packing ↔ `midnight-circuits` encoder | Mirrored constants (7×56), replay fixtures | + +## 9. Feature flags and build modes + +`evm` (revm + pinned-solc test harness), `solidity-trace` (LOG1 trace events; +trace renders also switch the external-evaluator staticcall to CALL), +`solidity-gas-checkpoints` (per-section LOG1 gas checkpoints, ~12k gas +overhead), `truncated-challenges` (128-bit x3/x1/x4 truncation — must match +the prover), `in-circuit-fewer-point-sets` / `outer-fewer-point-sets` / +`fewer-point-sets` (dummy-query PCS collapsing, split between recursive and +Solidity-facing proofs), `outer-single-h-commitment` (single-H quotient +layout, outer only), `rust-verifier-trace` (native trace hooks for +differentials). Trace/gas modes must not change verifier semantics; the +schema-changing trio must match the prover's build or all proofs are +(fail-closed) rejected. + +## 10. Testing and CI architecture + +Three tiers: (1) pure codegen tests (`src/lowering/tests.rs` ~4.0k lines, +`src/builder/tests.rs`) needing no EVM/SRS — VM differentials, manifest +ordering, layout facts, template-text pins; (2) env-gated EVM tests +(`src/test.rs` ~4.6k lines behind `HALO2_SOLIDITY_RUN_EVM_TESTS`, integration +fixtures for Poseidon/SHA/RSA/hybrid-MT, the k=20 IVC decider bench behind +`HALO2_SOLIDITY_RUN_IVC_BENCH`) — prove → render → pinned solc 0.8.30 → +Prague revm with blst-backed EIP-2537 → verify → mutate; (3) SRS-free replay +tests over committed fixtures, libFuzzer targets, bench/deploy scripts +(including live Sepolia deployment records under `deployments/`). + +CI (`.github/workflows/ci.yaml`): a default test job (EVM tests self-skip), +an EVM job running `cargo test ... pbt_` + the Poseidon fixture, and a trace +equivalence job; the heavy IVC benches and release size/hash gates live in a +weekly/push-to-main workflow (`solidity_verifier_bench.yml`). + +## 11. Documentation landscape + +37 files under `docs/{architecture,audit,benchmarks,plans,reference}`: a +bounded, falsifiable correctness claim with named exclusions +(`CODEGEN_ASSURANCE_DOSSIER.md`), a reviewer handoff packet, a rebuild-grade +verifier spec (`HALO2_MIDNIGHT_VERIFIER_SPEC.md`, 1.7k lines), memory-layout +and template-mapping references, reproducible-build manifest with published +runtime hashes, and honest limitation notes (the docs themselves flag the +memory-safe annotation as factually untrue and scope precisely what VM +certification does not prove). Parts of the audit chain have drifted from the +tree; the companion assessment enumerates the drift. + +## 12. Summary of architectural character + +The macro-architecture is disciplined and verified to hold in the imports: +facade → snapshot → converged plan → declarative render, with fail-closed +seams and unusually good rationale comments. The system's characteristic +strength is that security-relevant assumptions are *checked at generation +time* (SRS base, absorbed-G1 coverage, memory overlap, bytecode +certification, payload re-comparison) rather than trusted. Its characteristic +weakness is the residue of places where **strings and conventions do work +that types should do** (Yul text as an internal IR, comment-enforced enum +ordering, name-coupled injected code lines, five parallel operand decoders), +plus assurance-process gaps (CI test selection, artifact-recorded feature +profiles, panic-vs-Result at the public boundary). Those are quantified, with +verified evidence and prioritized redesign proposals, in +[`REDESIGN_PROPOSALS_2026-08.md`](../plans/REDESIGN_PROPOSALS_2026-08.md). diff --git a/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md b/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md index 2b1fea580..3dd62dba8 100644 --- a/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md +++ b/proofs/solidity-verifier/docs/architecture/LOWERING_ARCHITECTURE_SPEC.md @@ -21,7 +21,7 @@ templates, and provides helper APIs to repack native Halo2 proof bytes into the generated verifier ABI. For the audit-facing correctness and security argument, use -[`CODEGEN_ASSURANCE_DOSSIER.md`](./CODEGEN_ASSURANCE_DOSSIER.md). This +[`CODEGEN_ASSURANCE_DOSSIER.md`](../audit/CODEGEN_ASSURANCE_DOSSIER.md). This architecture document explains how the system is built; the dossier states the bounded claim, artifact manifest, threat model, and required evidence gates. @@ -63,22 +63,30 @@ pipeline. | --- | --- | | `src/lib.rs` | Public exports, feature flags, calldata helpers, EVM helpers. | | `src/api.rs` | Public configuration and diagnostic types, supported-shape errors, accumulator encoding metadata. | +| `src/evm.rs` | `verifyProof` calldata encoding, plus pinned-solc compilation and the `revm` harness behind the `evm` feature. | | `src/builder/` | Thin `SolidityGenerator` facade, constructor validation, public render/calldata/diagnostic wrappers. | +| `src/lowering/plan.rs` | `LoweringPlan`: the converged, reusable fact set every render/repack path is built from, plus post-convergence invariant checks. | | `src/lowering/artifacts.rs` | Verifier, VK, and quotient evaluator artifact assembly. | | `src/lowering/vk.rs` | Metadata convergence, VK payload generation, and static memory layout entrypoints. | | `src/lowering/calldata.rs` | Proof repacking and Solidity calldata encoding plan. | -| `src/lowering/quotient.rs` | Quotient identity planning, selector folds, and generated quotient blocks. | +| `src/lowering/config.rs` | Constants pinning the single generated verifier shape (VM prefix size, native callbacks, structured trash suffix, limb opcodes). | +| `src/lowering/diagnostics.rs` | Host-side diagnostics (evaluation counts, quotient identity manifest) derived from the same plan used for rendering. | +| `src/lowering/quotient.rs` | Quotient identity planning, selector folds, native callback selection, and generated quotient blocks. | | `src/lowering/protocol/` | Typed protocol plan derived from the verifying key. | | `src/lowering/abi/` | Static proof calldata and transcript-buffer layout. | | `src/lowering/render/` | Askama template data models and Yul formatting boundary. | | `src/lowering/layout/` | Static Yul memory planner, VK payload layout, numeric layout facts, and overlap validation. | | `src/lowering/kzg/` | KZG multi-open verifier emitter and pairing-input construction. | -| `src/lowering/quotient_numerator/yul_emit.rs` | Batched identity numerator reconstruction emitter. | -| `src/lowering/quotient_numerator/vm/` | Compact quotient numerator VM compiler and metadata. | +| `src/lowering/quotient_numerator/yul_emit.rs` | Batched identity numerator reconstruction emitter (inline Yul, not the VM). | +| `src/lowering/quotient_numerator/vm/mod.rs` | Compact quotient numerator VM compiler, opcode table, packed codec, and the physical-program validators. | +| `src/lowering/quotient_numerator/vm/reference.rs` | Independent interpreter for finalized VM bytecode, used only by certification. | +| `src/lowering/quotient_numerator/vm/certify.rs` | Render-time certification of emitted VM bytecode against the identity expressions it was lowered from. | | `src/lowering/encoding/` | Pointer, word, field, curve, calldata, and Yul formatting helpers. | | `templates/contracts/` | Generated Solidity contract templates consumed by Askama. | | `templates/partials/` | Shared Solidity/Yul fragments included by contract templates. | -| `tests/` | End-to-end rendering, EVM, trace, gas, and compatibility tests. | +| `tests/` | End-to-end fixture, EVM, trace, and compatibility integration tests. | +| `src/test.rs`, `src/lowering/tests.rs` | Gated Solidity/EVM verifier tests and codegen/layout unit tests. | +| `fuzz/` | `cargo-fuzz` targets for proof repacking and Solidity calldata. | | `docs/` | Grouped architecture, audit, benchmark, plan, and reference material. | The public API deliberately hides most of the planning machinery. Users usually @@ -337,18 +345,38 @@ Several generated sizes depend on earlier generated artifacts: - PCS dummy query count depends on protocol metadata and feature flags. - Template scratch requirements depend on finalized memory addresses. -`generator.rs` handles these dependencies with bounded convergence loops. It -builds provisional metadata, derives sizes, rebuilds with the new sizes, and -checks that the stable static layout has converged. If section sizes do not -settle within the allowed iteration count, generation fails instead of emitting -ambiguous bytecode. +`src/lowering/vk.rs` handles these dependencies with bounded convergence loops. +`generate_vk` reserves zero-filled quotient sections, recompiles against the +resulting layout, and repeats until the reserved sizes cover the compiled +program; `meta_data_for_stable_static_layout` iterates the VK memory base until +it matches the requirements derived from the resulting metadata. Both fail +loudly if they do not settle within the allowed iteration count, rather than +emitting ambiguous bytecode. + +`src/lowering/plan.rs` consumes the converged result. `LoweringPlan::new` +assembles it into one value and then re-checks the cross-module invariants that +convergence is supposed to establish: protocol/meta agreement, memory-region +non-overlap, absorbed-G1 precompile coverage, PCS memory requirements, and a +word-for-word comparison between the quotient constants and bytecode embedded in +the VK payload and the tables rebuilt from the plan. Rendering, diagnostics, and +proof repacking all read this same plan, so no call site can independently +re-derive a slice of the verifier shape. ## 12. Quotient Numerator Codegen -The quotient system in `src/lowering/quotient/` compiles Halo2 quotient numerator -logic into a compact VM representation. +The quotient system compiles Halo2 quotient numerator logic into a compact VM +representation. It is split across three modules: -The compiler: +- `src/lowering/quotient.rs` plans the identity stream for the concrete + verifying key: identity order, selector folds, which identities become native + Yul callbacks, and the execution manifest. +- `src/lowering/quotient_numerator/vm/mod.rs` is the producer. It lowers planned + identities into a compact byte-oriented program plus a constant table. +- `src/lowering/quotient_numerator/yul_emit.rs` is the inline Yul emitter for + the identities that are not lowered to bytecode, and for linearization-related + scalar preparation. + +The compiler in `vm/mod.rs`: - Lowers gate, permutation, lookup, and trash identities. - Assigns constants. @@ -358,20 +386,61 @@ The compiler: - Tracks memory tokens and native callback requirements. - Preserves the identity stream order expected by Midfall. -At runtime, the Yul quotient VM reconstructs the batched identity numerator. +At runtime, the Yul quotient VM in +`templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` reconstructs +the batched identity numerator. It is the only consumer of the emitted bytecode, +so the opcode set, operand widths, and memory-token map are an ABI between the +VK payload and that template rather than an implementation detail. Opcode and +token numbers reach the template through `template_constants.quotient_vm`, so +both sides read one Rust definition. + The emitted verifier stores the negative numerator value as the expected opening scalar, while the commitment side carries the `(1 - x^n)` factor. -The codebase uses two related but distinct pieces: - -- `quotient/`: compiler for compact numerator programs. -- `evaluator.rs`: Yul emitter for batched identity numerator reconstruction and - linearization-related scalar preparation. - The quotient evaluator is not an arbitrary Solidity subroutine. It is tied to a specific proof layout, VK layout, transcript schedule, and quotient program artifact. +### 12.1 Emitted-Bytecode Certification + +The lowering runs a peephole optimizer: shape recognizers rewrite seven-limb +foreign-field expressions into superinstructions, and a run-compaction pass +rewrites adjacent affine terms into counted opcodes. Both are pure encoding +choices that must preserve the evaluated polynomial exactly. + +Rather than trusting them, the generator proves it for the specific program each +render is about to emit. Before any artifact is produced: + +1. `validate_quotient_program` proves the byte stream decodes, that every memory + token is known, and that the expression stack is balanced; it returns the + maximum stack depth the memory planner then reserves. +2. `validate_quotient_const_slots` bounds-checks every constant-table index + against the emitted table, so a planner regression cannot make the deployed + verifier load a trailing VK word as a gate coefficient. +3. `certify::certify_quotient_program` executes the finalized bytecode with the + independent interpreter in `vm/reference.rs` at a deterministic assignment + derived from the artifact itself, and compares each identity against direct + evaluation of the `QuotientExpr` tree it was lowered from. +4. `certify::certify_quotient_builds_agree` rebuilds the same identity stream + with the limb superinstructions disabled — yielding a program of only + `PUSH`/`ADD`/`MUL`/`NEG` — and requires the two builds to agree + identity-by-identity. This turns every shape recognizer from trusted code + into a checked optimization. + +This is a generator-time gate, not a test. A recognizer bug on a previously +unseen gate shape fails the render instead of shipping a wrong verifier. + +Scope, stated precisely because it bounds what the gate proves: certification +covers the **emitter to reference-interpreter** leg. The +**reference-interpreter to Yul** leg is covered by the opcode/token table +conformance test in `src/lowering/tests.rs` — which checks that the template has +a `case` for every opcode, not that the case body is correct — and by the +per-identity native/Solidity trace differentials on fixture circuits. An opcode +that no fixture circuit emits therefore has unverified runtime semantics. +Identities executed as inline Yul, native callbacks, or the structured tail are +not lowered to bytecode at all and are covered only by those trace +differentials. + ## 13. PCS Codegen `src/lowering/kzg/mod.rs` emits the KZG multi-open verifier logic. It mirrors the @@ -510,28 +579,39 @@ The repository tests the generator through several layers: - Gas checkpoint builds for section-level profiling. - IVC and Moonlight integration benches for realistic proof shapes. -Representative local commands: +Which of these run automatically is part of the design, not an afterthought. +`.github/workflows/ci.yaml` gates every pull request on three jobs: +`test-solidity-verifier` (codegen, layout, and template invariants, with the +heavy proof/EVM cases self-skipping), `test-solidity-verifier-evm` (adversarial +property tests plus the Poseidon end-to-end fixture), and +`test-solidity-verifier-trace` (Poseidon native/Solidity trace equivalence). +The two IVC proof benches and the release bytecode size/hash gate are too slow +for a per-PR job and live in `.github/workflows/solidity_verifier_bench.yml`, +which runs on pushes to `main`, weekly, and on demand. Trigger that workflow +against a branch before merging changes to memory layout, proof layout, the +quotient VM, or the templates. + +Representative local commands, run from the Midfall repository root: ```bash -cargo test --lib +cargo test -p halo2_solidity_verifier --all-features --all-targets -- --nocapture ``` ```bash -cargo test --features evm --test codegen +HALO2_SOLIDITY_RUN_EVM_TESTS=1 \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +cargo test -p halo2_solidity_verifier --release --all-features pbt_ -- --nocapture ``` ```bash -MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ -cargo test --manifest-path /Users/Julien.Coolen/Moonlight/aggregation/Cargo.toml \ - wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs --release \ - --lib -- --ignored --nocapture +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ +proofs/solidity-verifier/scripts/run_ivc_bench.sh ``` -The Moonlight command runs from this repository's Solidity verifier integration -when Moonlight has a local path dependency pointing back to this checkout. It -generates Moonlight wrap proof material, repacks it for the Solidity verifier, -deploys the generated verifier path, and checks the proof on-chain in the local -EVM harness. +Solidity-touching tests require `solc 0.8.30+commit.73712a01` on `PATH` or via +`SOLC`; `scripts/install_pinned_solc.sh` fetches it. The full command inventory, +including the Moonlight wrap-recursion bench against a sibling Moonlight +checkout, is in the [README](../../README.md). ## 20. Extension Guidelines @@ -557,12 +637,25 @@ For a new VK section: For new Yul scratch memory: -1. Add a named range to `VerifierMemoryLayout`. -2. Assign the correct `MemoryPhase`. +1. Add a named range to `VerifierMemoryLayout` in + `src/lowering/layout/memory.rs`. +2. Assign the correct `MemoryPhase`, keeping the enum in runtime order — its + derived `Ord` is what makes `MemoryLifetime::intersects` answer the right + question. 3. Let the overlap checker validate reuse. -4. Thread the address through `template.rs`. +4. Thread the address through the Askama models in + `src/lowering/render/models.rs`. 5. Avoid hard-coded addresses in templates. +Note the limit of step 3: `MemoryMap::validate` proves that *registered* regions +do not conflict when live. It cannot prove that the emitted Yul only touches +registered regions — that agreement is pinned by rendered-source tests such as +`lagrange_denominator_run_uses_registered_scratch_region`, which asserts the +Lagrange block bases its batch-inversion input run at the registered +`lagrange_denoms` region rather than a theta slot. All generated scratch is +planner-registered; add new scratch as a registered region and pin its use +with a rendered-source test. + For quotient VM changes: 1. Extend the opcode, token, or constant model in Rust. @@ -602,9 +695,11 @@ proof order, memory addresses, transcript absorbs, or public input packing. This specification is the architectural overview. More focused details live in: -- `docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md` -- `docs/architecture/MEMORY_LAYOUT.md` -- `docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md` -- `docs/reference/QUOTIENT_NUMERATOR_EVALUATOR.md` -- `docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md` -- `docs/reference/TEAM_DEMO_SETUP.md` +- [`HALO2_MIDNIGHT_VERIFIER_SPEC.md`](../reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md) +- [`MEMORY_LAYOUT.md`](./MEMORY_LAYOUT.md) +- [`ASKAMA_TEMPLATE_RUST_MAPPING.md`](../reference/ASKAMA_TEMPLATE_RUST_MAPPING.md) +- [`QUOTIENT_NUMERATOR_EVALUATOR.md`](../reference/QUOTIENT_NUMERATOR_EVALUATOR.md) +- [`QUOTIENT_EVALUATOR_9KB_BYTECODE.md`](../reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md) +- [`TEAM_DEMO_SETUP.md`](../reference/TEAM_DEMO_SETUP.md) +- [`CODEGEN_ASSURANCE_DOSSIER.md`](../audit/CODEGEN_ASSURANCE_DOSSIER.md) +- [`REPRODUCIBLE_BUILDS.md`](../reference/REPRODUCIBLE_BUILDS.md) diff --git a/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md b/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md index 4550d1607..0a71e7d81 100644 --- a/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md +++ b/proofs/solidity-verifier/docs/architecture/MEMORY_LAYOUT.md @@ -27,10 +27,35 @@ Solidity reserves the first four words of memory for compiler conventions: The generated verifier intentionally does not follow Solidity allocation by reading and bumping `mload(0x40)`. Instead, every generated absolute memory -region is planned at or above `0x80`. The streaming transcript buffer, the main -verifier return word, the split quotient return frame, the VK constructor -payload buffer, and low-memory precompile scratch all start from named -Rust-side layout constants rooted at `SOLIDITY_ALLOCATABLE_MEMORY_START`. +region is planned from named Rust-side layout constants. + +Those constants are **not** rooted at `0x80`. The verifier body is wrapped in +`assembly ("memory-safe")`, which is load-bearing -- without it the block does +not compile under `--via-ir` (stack too deep) -- but also factually untrue, +since the block writes memory it never obtained from the free-memory pointer. +The annotation is what enables solc's stack-to-memory mover, which reserves +spill slots upward from `0x80` and records the top in the runtime's +`mstore(0x40, ...)` prologue. Observed reservations range from `0x80` (none) to +`0x8e0`, varying with the circuit, the solc release, and the optimizer +schedule. + +Basing the layout at `0x80` therefore put solc's spill slots and the verifier's +own transcript buffer in the same bytes, separated only by live ranges that +nothing enforced -- a recompilation could silently place a live spill across a +verifier write and corrupt a challenge or pairing input. So the streaming +transcript buffer, the main verifier return word, the split quotient return +frame, low-memory precompile scratch, the accumulator/KZG pairing-batch hash +frame, and the final two-pair pairing frame are rooted at +`LOW_MEMORY_SCRATCH_START` (`0x1000`), above the largest observed reservation. +`VerifierMemoryLayout::validate()` rejects any generated region below that +base, and `compiled_memoryguard_does_not_overlap_generated_layout` compiles +each rendered variant -- including the accumulator-bearing ones -- and fails +the build if a future circuit or compiler pushes the reservation past it. + +The VK constructor payload buffer is the exception: it lives in +`Halo2VerifyingKey`, whose assembly carries no `memory-safe` annotation, so +solc reserves nothing there and it stays at +`SOLIDITY_ALLOCATABLE_MEMORY_START`. The code generator treats `[0x00..0x80)` as off limits for generated writes: `VerifierMemoryLayout::validate()` rejects any registered region inside that @@ -140,14 +165,16 @@ which changes the transcript-buffer bound. The verifier reserves: - unaligned starts or lengths; - any generated region inside Solidity-reserved memory `[0x00..0x80)`; +- any generated region below `LOW_MEMORY_SCRATCH_START` (`0x1000`), where a + live via-IR spill slot could share its bytes; - overlapping permanent regions; - overlapping scratch regions that are live in the same `MemoryPhase`; - PCS fixed-window overflows. Intentional reuse is represented by giving the same byte range disjoint -lifetimes. For example, `batch_invert_scratch`, quotient VM scratch, q_eval -source tables, q_com trace scratch, and final MSM scratch can share bytes when -their phases do not overlap. +lifetimes. For example, `batch_invert_scratch`, `lagrange_denoms`, quotient VM +scratch, q_eval source tables, q_com trace scratch, and final MSM scratch can +share bytes when their phases do not overlap. The `trace_u256` log word is deliberately not a historical fixed constant. Trace hooks can run between reads from long-lived VK/eval/commitment memory, so @@ -210,6 +237,12 @@ addresses. The offsets below are in 32-byte words from `THETA_MPTR`. The gaps between fixed windows are deliberate historical padding. Do not use them as scratch without registering a `MemoryRegion` and a phase. +The Lagrange batch-inversion input run historically wrote its denominators in +place starting at `x_n` (word 26), overlaying words 27..51 and spilling into +`rot_points` for large instance counts. The run now lives in the registered +`lagrange_denoms` phase region above the decompressed commitments; word 26 is +a plain one-word permanent slot again. + ## Dynamic Commitment Region The commitment region begins at: @@ -245,16 +278,16 @@ The planner validates by lifetime, not just by address. | Phase | Region examples | Notes | | --- | --- | --- | -| `Transcript` | `[0, transcript_words * 0x20)` | Must stay below `VK_MPTR`. | +| `Transcript` | `[0x1000, 0x1000 + transcript_words * 0x20)` | Must stay below `VK_MPTR`. | | `ScalarInv` | `VK_MPTR - 0x100` frame | Historical modexp scratch near the VK payload. | -| `LagrangeBatchInvert` | `batch_invert_scratch_mptr` | Reuses selector bytes before selector accumulators are live. | +| `LagrangeBatchInvert` | `batch_invert_scratch_mptr`, `lagrange_denoms_mptr` | Prefix-product scratch plus the batch-inversion input run; both reuse selector/quotient bytes before those phases are live. | | `QuotientVm` | quotient temps and stack | Used before PCS final MSM. | | `PcsQEvalSourceTable` | rolled q_eval address table | Aliases `pcs_scratch_mptr`. | | `PcsQComTrace` | optional q_com trace MSM | Aliases `pcs_scratch_mptr`; trace-only. | | `PcsFinalMsm` | final MSM input and selector accumulators | Selector accumulators and final MSM must not overlap in this phase. | | `AccumulatorMsm` | public accumulator MSM input | Length is derived from accumulator/VK shape. | -| `AccumulatorPairingBatch` | `[0x100, 0x320)` | Hash domain plus four G1 points for accumulator pairing batching. | -| `FinalPairing` | two-pair KZG pairing frame | Low-memory final precompile frame. | +| `AccumulatorPairingBatch` | `[0x1000, 0x1220)` | Hash domain plus four G1 points for accumulator pairing batching. | +| `FinalPairing` | `[0x1220, 0x1540)` two-pair KZG pairing frame | Final precompile frame, placed past the pairing-batch frame by construction. | ## Update Rules diff --git a/proofs/solidity-verifier/docs/architecture/MIGRATION.md b/proofs/solidity-verifier/docs/architecture/MIGRATION.md index 5e314d953..237132060 100644 --- a/proofs/solidity-verifier/docs/architecture/MIGRATION.md +++ b/proofs/solidity-verifier/docs/architecture/MIGRATION.md @@ -343,6 +343,10 @@ The `cargo test --lib` suite now stands at 7/7 green: `assembly ("memory-safe") { ... }` to silence the legacy stack-too-deep error path; with `--via-ir` solc 0.8.30 now compiles the full ~117 kB output cleanly. + Note the annotation is not merely cosmetic: it enables solc's + stack-to-memory mover, which reserves spill slots upward from + `0x80`. See AUDIT.md TA-5 and `docs/architecture/MEMORY_LAYOUT.md` + for why the generated layout is based at `0x1000` rather than `0x80`. * `tests/poseidon_fixture.rs` — new integration test (gated behind `feature = "evm"` and currently `#[ignore]`d, see below) that: 1. Configures `SRS_DIR` to point at diff --git a/proofs/solidity-verifier/docs/audit/AUDIT.md b/proofs/solidity-verifier/docs/audit/AUDIT.md index b85c509aa..df8a5d3ee 100644 --- a/proofs/solidity-verifier/docs/audit/AUDIT.md +++ b/proofs/solidity-verifier/docs/audit/AUDIT.md @@ -1,5 +1,27 @@ # CTF Vulnerability Analysis: Halo2 Solidity Verifier +> **Path-migration note (2026-08-12).** The findings below were written +> against the pre-rename source tree. `src/codegen/` no longer exists; it was +> refactored into `src/lowering/`. The historical citations (including old +> line numbers) are preserved verbatim as the audit record; to re-verify a +> finding against the current tree, translate paths with this map: +> +> | Historical path | Current location | +> | --- | --- | +> | `src/codegen.rs`, `src/codegen/mod.rs` | `src/lowering/mod.rs` (pipeline root), `src/lowering/plan.rs` (planning), `src/lowering/artifacts.rs` (artifact assembly) | +> | `src/codegen/generator.rs` | `src/lowering/plan.rs` + `src/lowering/artifacts.rs` + `src/lowering/render/` | +> | `src/codegen/template.rs` | `src/lowering/render/models.rs` | +> | `src/codegen/protocol.rs` | `src/lowering/protocol/mod.rs` | +> | `src/codegen/proof_layout.rs` | `src/lowering/abi/proof.rs` | +> | `src/codegen/evaluator.rs` | `src/lowering/quotient_numerator/{mod,yul_emit,vm/mod}.rs` | +> | `src/codegen/quotient/mod.rs` | `src/lowering/quotient.rs` + `src/lowering/quotient_numerator/vm/mod.rs` | +> | `src/codegen/pcs.rs` | `src/lowering/kzg/mod.rs` | +> | `src/codegen/util.rs` | `src/lowering/encoding/mod.rs` + `src/lowering/layout/mod.rs` | +> | `src/transcript.rs` | `templates/partials/verifier/TranscriptProofParser.yul` + `src/lowering/abi/` | +> +> `templates/contracts/Halo2Verifier.sol` still exists but is now assembled +> from the partials in `templates/partials/verifier/`. + ## 2026-05-02 audit addendum: PCS scratch layout and instance-column shape Scope: current dirty worktree for the Halo2/Midnight Solidity verifier @@ -798,7 +820,7 @@ The current `templates/contracts/Halo2Verifier.sol` blocks this path via `AUTHOR F-9 │ Low │ `if mload(HAS_ACCUMULATOR_MPTR)` is read from VK but it is not cross-checked against the codegen-side `acc_encoding` I-10 │ Informational │ Generator forces `vk.cs().num_instance_columns() <= 1` and `Rotation::cur()` only - silent "not yet implemented" panics if violated I-11 │ Informational │ `n_inv` and `omega_inv_to_l` are never re-derived from `k`/`omega` on chain - a malicious VK that bypasses the codehash pin can lie - I-12 │ Informational │ `mod(hash, r)` introduces ~2^-255 bias; standard practice, kept for completeness + I-12 │ Informational │ `mod(hash, r)` is measurably biased (1.5x max/min density, ~0.075 statistical distance), not ~2^-255; bounded soundness cost, no action The mitigations that have already landed (in particular the AUTHORIZED_VK + EXPECTED_VK_CODEHASH pinning at constructor time, commit 54b2943) close the original "caller-controlled VK" hole from AUDIT.md finding #1, and the EIP-2537 pairing precompile transitively covers G2 subgroup checks (BN254 audit finding #2). @@ -1056,7 +1078,41 @@ The current `templates/contracts/Halo2Verifier.sol` blocks this path via `AUTHOR I-12 - Bias in `mod(hash, r)` - r = 0x73eda7…00000001 ≈ 2^254.86. With a 256-bit hash, the bias is ~2^256 / r - 1 ≈ 2^-254, completely negligible. Standard practice; no action. + Correction. An earlier revision of this item stated "the bias is ~2^256 / r - 1 ≈ 2^-254, completely negligible". That is wrong: 2^256 / r - 1 = 1.208, not 2^-254. The + arithmetic only yields a negligible quantity when r ≈ 2^256, and here r ≈ 2^254.86, so 2^256 / r ≈ 2.208. The conclusion (informational, no action) still holds, but for a + different reason and with a much larger constant than the original text implied. The numbers below are the actual ones. + + Where. `sample_keccak_digest_be_mod_r` (midfall/proofs/src/transcript/implementors.rs:19) reduces one 256-bit Keccak digest mod r: + `BigUint::from_bytes_be(&hash_output) % modulus`. The generated Yul mirrors it exactly in `squeeze_to` + (templates/partials/verifier/AssemblyHelpers.yul, `mstore(mptr, mod(h0, r))`). The bias is therefore inherited from the native Midfall transcript, not introduced by this + codegen, and the two sides agree bit-for-bit. Removing it would be a transcript change on both sides, not a Solidity-only fix. + + Magnitude. With r = 0x73eda753…00000001: + + r / 2^256 = 0.452845 + floor(2^256 / r) = 2 + R = 2^256 mod r = 0.094310 * 2^256 + + So residues in [0, R) have 3 preimages under the reduction and residues in [R, r) have 2. That gives: + + max/min density ratio = exactly 1.5 + statistical distance ≈ 0.0747 + max density / uniform = 3r / 2^256 ≈ 1.3585 (0.442 bits) + + The last line is the quantity that enters a soundness reduction: any event has probability at most 1.3585x its probability under a uniform challenge. The 1.5 figure is the + spread between the most and least likely residue, which is a valid but looser bound. + + Impact. Ten challenges are squeezed. Three do not consume a full-width sample: `x3` is truncated to 128 bits at squeeze time, and `x1` / `x4` are consumed only through + truncated powers (see the `truncated-challenges` feature). The remaining seven - theta, beta, gamma, trash_challenge, y, x, x2 - are used at full width. Compounding the + per-challenge factor over those seven: + + tight 1.3585^7 ≈ 8.5x (3.09 bits) + loose 1.5^7 ≈ 17.1x (4.09 bits) + + Against a ~254.86-bit challenge space, that moves a 2^-254 soundness error to roughly 2^-251. Negligible in absolute terms, which is why this stays Informational. + + Recommendation. No action for the current deployment. If the transcript is ever revised, sample challenges by rejection or by reducing a wider digest (e.g. 512 bits, giving + statistical distance < 2^-250), and change the Rust and Yul sides together. Do not "fix" only the Solidity side: the two must produce identical challenges. ────────────────────────────────────────── @@ -2120,6 +2176,13 @@ arithmetic assumes valid curve points. ### TA-1. Accumulator Points With Scalar Zero Bypass Curve/Subgroup Validation Severity: Medium/High, depending on circuit assumptions. +Status: Fixed. The quoted `switch lhs_scalar` construct no longer exists; +`templates/partials/verifier/AccumulatorHelpers.yul:277-293` (and the RHS +mirror at `:339-347`) unconditionally routes every decoded accumulator point +through EIP-2537 G1MSM — including identity encodings and zero/one scalars — +with an explicit comment stating that the precompile is the curve/subgroup +validator and that skipping it would let a malformed point hide behind +scalar 0. Triaged 2026-08-12. In `load_acc_point`, accumulator coordinates are decoded from public instances into `ACC_LHS_MPTR` and `ACC_RHS_MPTR`. The decoded point is range/canonical @@ -2195,6 +2258,13 @@ Recommendation: ### TA-2. Transcript Must Be Proven To Bind Every Verifier-Side Fixed Input Severity: Medium. +Status: Open. `vk_digest` covers the native VK contents but not the SRS +points, quotient VM program, accumulator schema constants, or feature +profile; widening its coverage is a protocol change affecting the prover and +is tracked as review item M-4 (out of scope for the 2026-08 fix set — +explicitly deferred). Partially mitigated at build time by the SRS tau +binding in `src/lowering/vk.rs` and on-chain by the VK codehash pin plus the +generated-constant cross-checks after `extcodecopy`. Triaged 2026-08-12. The verifier absorbs `vk_digest` rather than the full VK payload: @@ -2243,6 +2313,12 @@ Recommendation: ### TA-3. External Quotient Evaluator Is Pinned, But Not Transcript-Bound Severity: Low/Medium. +Status: Open. Inert for single-contract renders (e.g. the moonlight-wrap +fixture, which ships no `Halo2QuotientEvaluator.sol`), but LIVE for split +renders: the IVC fixture (`target/ivc-keccak-solidity-dump/`) ships a +`Halo2QuotientEvaluator.sol` and relies on the codehash pin alone. Folding +the evaluator identity into the transcript is part of the same digest +discussion as TA-2/M-4. Triaged 2026-08-12. `AUTHORIZED_QUOTIENT` is codehash-pinned, which is good. However, its output directly determines: @@ -2266,6 +2342,13 @@ Recommendation: ### TA-4. Gas Checkpoint Logs Make This Unsuitable As A Production Verifier Severity: Low/Medium. +Status: Resolved / diagnostic-only. Gas checkpoints are emitted only when +the `solidity-gas-checkpoints` feature sets +`SOLIDITY_GAS_CHECKPOINTS_ENABLED`; the feature is off by default and the +`production_renders_do_not_emit_gas_checkpoints` test asserts production +renders contain no LOG1 and stay `external view`. Bench profiles that enable +it are documented as non-production in REPRODUCIBLE_BUILDS.md. Triaged +2026-08-12. `verifyProof` can include gas checkpoint logging: @@ -2303,6 +2386,12 @@ Recommendation: ### TA-5. Dangerous Reliance On `assembly ("memory-safe")` With Absolute Memory Ownership Severity: Low/Medium. +Status: Fixed by P2 (commit `460a666`): the false `"memory-safe"` annotation +was removed from the main verifier block and a free-memory-pointer guard +(`if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) }`, +`templates/contracts/Halo2Verifier.sol:117`) now enforces the layout +assumption at runtime instead of asserting it to the optimizer. Triaged +2026-08-12. The main assembly block is marked `"memory-safe"` while it intentionally owns the entire call-frame memory, writes to low memory, uses fixed absolute @@ -2314,10 +2403,33 @@ memory-safety rules. If future edits add Solidity code after the block, or if the compiler reasons across the block in an unexpected way, this becomes a miscompilation risk. +**Status: addressed.** The risk was not hypothetical. `solc 0.8.30` with the +pinned flags emits `mstore(0x40, 0x08e0)` for the moonlight-wrap render, i.e. +it reserved `[0x80, 0x8e0)` for via-IR stack-to-memory spill slots -- directly +on top of a generated layout that started at `0x80`. Reservations observed +across renders: `0x80` (ivc-keccak, none), `0xe0` (rsa), `0x3c0` (poseidon), +`0x8e0` (moonlight-wrap). At least one spill (`mstore(0x300, mload(0x6a00))`, +the `y` challenge, re-read ~600 IR lines later) sits in that window. + +The recommendation below to remove the annotation was tested and does not work: +the block then fails to compile with `Cannot swap Variable usr$f_4 ... too deep +in the stack by 1 slots`, and solc itself suggests re-adding the annotation. +The annotation is load-bearing. + +The fix instead moves the generated layout above the reservation +(`LOW_MEMORY_SCRATCH_START = 0x1000`), making the two regions disjoint in space +so their liveness no longer matters, and adds +`compiled_memoryguard_does_not_overlap_generated_layout`, which compiles each +rendered variant and fails if the reservation ever grows past that base. Note +this removes the *consequence*, not the false annotation itself; making the +annotation honest would require runtime `mload(0x40)`-based re-basing at the +cost of an `ADD` per memory access. + Recommendation: -- Prefer removing `"memory-safe"` from the terminal verifier block unless there - is a compiler-specific proof that this pattern is accepted. +- ~~Prefer removing `"memory-safe"` from the terminal verifier block unless + there is a compiler-specific proof that this pattern is accepted.~~ + Superseded: removal does not compile. See status note above. - Pin the exact compiler and EVM version. The Renegade audit's recommendation to use fixed pragmas rather than floating `^0.8.x` is especially relevant for generated verifier code. @@ -2332,6 +2444,12 @@ Recommendation: ### TA-6. Precompile Smoke Tests Are Too Weak For Deployment Confidence Severity: Low. +Status: Fixed by P5 (commit `460a666`): the constructor probes now include +known-answer vectors a stub cannot satisfy — G1ADD(G, G) == 2G, G1MSM +[2]*G == 2G, a NEGATIVE G1MSM probe with an on-curve but out-of-subgroup +point that must be rejected, and pairing probes with both a true and a false +product (`templates/partials/verifier/PrecompileSmoke.sol`). Triaged +2026-08-12. The constructor checks identity inputs for G1ADD, G1MSM, and pairing. That catches absent precompiles and gross return-size issues, but it does not catch: @@ -2354,6 +2472,12 @@ Recommendation: ### TA-7. Root-Of-Unity / Zero-Denominator Cases Revert Rather Than Being Specified Severity: Informational/Low. +Status: Open (documentation). Behavior is fail-closed by construction — +`batch_invert` and `scalar_inv` return failure/revert on any denominator +congruent to zero, and `verifier_rejects_when_x_is_forced_to_domain_root` +covers the x-at-domain-root case — but the spec does not yet state this as +the intended semantics. Needs a paragraph in +`docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md`. Triaged 2026-08-12. The Lagrange and PCS blocks intentionally batch-invert values like: @@ -2383,6 +2507,14 @@ Recommendation: ### TA-8. Raw Verifier Integration Can Still Be Replayed/Misused By Application Contracts Severity: Integration risk. +Status: Acknowledged / integration guidance. The raw verifier is stateless +by design and cannot bind proof meaning itself; the wrapper obligations are +now stated as REQUIREMENTS (replaceable verifier address, wrapper-held +pause, chainid + wrapper-address + anti-replay binding) in +`docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §2, together with the +incident-response and migration playbook. Any deployment review must check +the wrapper against that document, not just this verifier. Triaged +2026-08-12; requirements doc added 2026-08-13. The verifier correctly says application contracts must bind the meaning of public instances separately. That warning is important. This raw verifier diff --git a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md index 0986048bf..13603b895 100644 --- a/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md +++ b/proofs/solidity-verifier/docs/audit/AUDIT_FINDINGS.md @@ -1,5 +1,12 @@ # Solidity Verifier Codegen Audit +> **Path-migration note (2026-08-12).** These findings were written against +> the pre-rename source tree; `src/codegen/` has since been refactored into +> `src/lowering/`. Historical `**File:**` citations (including line numbers) +> are preserved as the audit record; translate with the migration map at the +> top of `AUDIT.md` when re-verifying. The release-facing status table below +> has been updated to cite current code and test names. + **Repository:** `halo2-solidity-verifier-exp` **Scope:** halo2 → midnight-proofs port of the on-chain KZG/BLS12-381 verifier codegen and emitted Yul. @@ -47,7 +54,7 @@ the release-facing status snapshot as of 2026-05-11. | 2026-05-11 #1 lookup quotient identity count | Fixed | `lookup_chunks.iter().map(|chunks| chunks + 2).sum()` is used in planning and validation; `lookup_identity_source_handles_variable_chunk_counts` covers variable chunk counts. | | 2026-05-11 #2 fixed eval count | Fixed | `proof_evaluation_counts().fixed` counts `EvalRead::Fixed` entries from the protocol plan instead of fixed columns. | | 2026-05-11 #3 challenge phase remapping | Fixed | `ProtocolPlan::from_constraint_system` sizes phases by the max of advice and challenge phases; `plan_allows_challenge_phase_beyond_advice_phases` covers this case. | -| 2026-05-11 #4 packed32 operand widths | Fixed | `validate_packed_quotient_operand` enforces logical `u8` and `u16` bounds; `packed32_validator_rejects_logical_operand_width_corruption` covers corrupted packed operands. | +| 2026-05-11 #4 packed32 operand widths | Fixed | Operand decoding and width bounds are enforced by `validate_quotient_program` / `validate_quotient_const_slots` / `validate_quotient_mem_ptrs` in `src/lowering/quotient_numerator/vm/mod.rs`; `quotient_vm_safety_validator_rejects_malformed_programs` covers stack underflow, unknown memory tokens, and truncated operands, and `quotient_vm_lengths_are_derived_from_opcode_spec` pins operand widths to the opcode spec. (The names previously cited here — `validate_packed_quotient_operand` and `packed32_validator_rejects_logical_operand_width_corruption` — never landed under those identifiers; corrected 2026-08-12.) | | 2026-05-11 #5 reserved memory writes | Fixed | Trace and helper templates avoid Solidity-reserved memory writes; `templates_do_not_write_solidity_reserved_memory_slots` checks `mstore(0,`, `mstore(0x00,`, and related reserved forms. | | 2026-05-11 #6 external quotient return overlap | Fixed | External quotient output uses `QUOTIENT_RETURN_MPTR`; template validation checks output length and disjointness from the copied quotient frame. | | 2026-05-11 #7 structured selector-run trace | Fixed | Selector-run grouping is disabled when trace is enabled, preserving per-identity trace events through direct quotient blocks. | @@ -1416,4 +1423,98 @@ These should be fixed before treating the document as a conformance spec. The rest are mostly edge-case, interoperability, or validation issues, but several could still become soundness bugs in a generated verifier. + +## 2026-08-13 — independent external review (MF series) + +An independent review of the rendered `moonlight-wrap` artifacts (verifier +sha256 `555ed976…6798`, VK `7ca78ec2…b7ec`; byte-identical to +`fixtures/moonlight-wrap/`) run as three separate passes — cryptographic +implementation, ZK/protocol soundness, and EVM/software security — plus a +line-by-line equivalence pass against `midfall/proofs` and machine +recomputation of every recomputable constant. + +**No soundness-relevant defect was found.** The transcript schedule is +byte-exact against the Rust verifier, every absorbed proof point provably +reaches a subgroup-enforcing precompile, the linearization/PCS algebra is +exact, and the memory plan has no live overlaps. Findings are one liveness +bug and a set of hardening/diagnostic items. + +| ID | Sev | Finding | Disposition | +| --- | --- | --- | --- | +| MF-1 | High | `MODEXP_GAS = 1360` is the EIP-2565 price; EIP-7883 (Osaka/Fusaka) prices the same frame at 4064, so every proof reverts `PrecompileFailed` on a repriced chain — and the constructor never probed modexp, so deployment succeeded silently | **Fixed** (`modexp_gas_word_frame` takes the max over live schedules; constructor modexp known-answer probe added) | +| MF-2 | Low | The memory-layout guard — the only on-chain check for a bad recompile — reverted bare | **Fixed** (`MemoryLayoutViolated()`, typed on the runtime and constructor paths) | +| MF-3 | Low | Quotient VM: fold-on-empty re-folds a stale top; native callbacks reset `q_sp` instead of asserting it, hiding dropped operands; u16 const indexes unclamped; `q_sp` unbounded | **Fixed** (four guards; u8 indexes keep their documented exemption, u16 do not) | +| MF-4 | Low | `PrecompileFailed` / `BadPointEncoding` / `ProofRejected` conflated chain faults with rejected input on three paths | **Fixed** (cause flags threaded through `batch_invert` and `validate_public_accumulator`; `ec_pairing` split; triage table in `DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §6) | +| MF-5 | Info | A low-level `staticcall` to an address with no code returns success — a mis-wired wrapper reads it as a valid proof | **Documented** (wrapper obligation W-4) | +| MF-6 | Info | Single-reduction `mod r` sampling bias (max point mass ≈1.36× uniform) | **Accepted**, parity with the Rust reference; the sampling-bias factor is derived in `AUDIT.md`. Regenerate in lockstep if the reference moves to 512-bit reduction | +| MF-7 | Info | 128-bit truncated challenges cap batching soundness | **Accepted**: the profile mirrors the midnight-proofs prover, so a verifier-side change alone is impossible | +| MF-8 | Info | Set-0 has 43 eval terms but 42 commitment terms (the committed-instance column's commitment is the identity), so its eval is forced ≈0 only indirectly via x1-batching | **Documented** in the PCS emitter | +| MF-9 | Info | The canonical identity accumulator `(O, O)` is well-formed and passes the pairing layer | **Documented** (wrapper obligation W-5) | +| MF-10 | Info | Native-identity selector folds appeared to hardcode a `y¹` gap | **WITHDRAWN — false positive.** All three emission sites already use `selector_fold.gap_for(identity)`; the fixed `Some(1)` is confined to `native_identity_estimate_block`, a size/gas proxy for gate-selection that is never emitted, and whose doc comment explicitly warns against "fixing" it to call `gap_for` (the fold plan is derived from the selection outcome, so it does not exist yet at that point). Recorded so the warning is not overridden by a future reviewer making the same mistake. | +| MF-11 | Info | The transcript stream is positionally framed (a point and four scalars are byte-identical) | **Accepted**; identical to §5.1 (I-5). Any future variable-length section would need explicit length tags | +| MF-12 | Info | `assembly ("memory-safe")` is unsound by the letter of the annotation | **Accepted**; safe here via the terminal block + FMP guard + pinned pragma, now noted at the annotation site | + +Also proposed by the review and **withdrawn on inspection**: a CI job +recomputing `vk_digest` from the rendered VK payload. The payload word is +written directly from `self.vk.transcript_repr()` in `lowering/vk.rs` — a +single expression over a single in-memory VK, not two independent paths — so +such a test would assert `x == x`. The residual it was meant to close (that +`vk_digest` binds the *semantic* constraint system) is not reachable this way; +it is covered by the trace-replay tests and, off-transcript, by `BUILD_ID`. +See §5.2 for the standing M-4 decision. + +**Verification.** The full EVM-gated suite runs green against these changes: +218 passed / 0 failed under `--features evm,rust-verifier-trace`, including +native-Rust-vs-Solidity trace equivalence, the constructor precompile-rejection +tests, `typed_errors_identify_rejection_classes`, and +`compiled_verifier_runtime_fits_the_eip170_limit` — so the added probe and +guards do not breach the code-size limit. The MF-4 Lagrange re-route is +observed executing rather than merely pinned in template text: the +forced-domain-root verifier reverts with `ProofRejected()`. + +MF-1's probe is likewise demonstrated rather than asserted. +`constructor_rejects_a_modexp_bound_below_the_chain_price` renders the verifier, +lowers `MODEXP_GAS` to one gas below what this harness's revm charges for the +frame, and requires DEPLOYMENT to fail -- with a positive control deploying the +same fixture unmutated, so the rejection cannot pass vacuously. That is the +exact shape of the original bug (the shipped 1360 sat below EIP-7883's 4064), +reproduced by moving the bound rather than the schedule, because the pinned +revm 19 exposes `SpecId::OSAKA` but still prices modexp with `berlin_run`. + +Caveat on the compiler: the pinned NATIVE solc binary was not reachable from +the environment that applied these fixes, so the suite ran against the same +compiler commit built to WASM (npm `solc` 0.8.30, reporting +`0.8.30+commit.73712a01`) behind a shim exposing the native CLI surface. That +is sound for exercising behaviour, but this repository's reproducibility claim +pins the native binary's sha256 — so nothing produced that way may be deployed +or used to pin a hash. + +The two builds were then compared directly, which upgrades that caveat from an +assumption to a measurement: recompiling `deployments/sepolia/moonlight-wrap/ +Halo2Verifier.sol` (a 21,161-byte via-IR runtime, at the `deployment.json` +settings) through the WASM build reproduces the committed native-compiled +runtime with exactly 40 differing bytes — the two 20-byte immutable +`AUTHORIZED_VK` slots, which a fresh compile leaves zeroed. All 21,121 other +bytes match. So the code-size result carries native weight; re-confirming it on +a pinned-binary host remains good release hygiene rather than an open risk. + +**Correction to an earlier claim in this section's history.** The MF-1 commit +message and an earlier revision of `DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §4 +stated that the `deployments/sepolia/moonlight-wrap` deployment should be +assumed bricked post-Fusaka. That is wrong, and the assumption behind it was +wrong: that deployment predates the exact-gas hardening, so all 17 of its +`staticcall`s — the three modexp sites included — forward `gas()`, and an +upward repricing is absorbed rather than fatal. Only artifacts carrying exact +bounds are exposed, which is the population MF-1 addresses. Verified by +reading the recorded source (its recompiled runtime reproduces +`deployment.json`'s `runtimeCodeHash` byte-for-byte apart from the two +immutable `AUTHORIZED_VK` slots), so no `eth_call` is needed to settle it. + +**Not closed by these commits:** the committed fixtures predate the MF-1 fix +and are marked STALE; regenerating them needs the SRS asset (unreachable here) +and, for moonlight-wrap, a Moonlight checkout. +The replay harness also cannot exercise EIP-7883 — the pinned revm 19 has an +Osaka spec, but its modexp handler is still `berlin_run` — so real coverage +awaits a revm bump; `src/evm.rs` records that gap at the `SpecId` pin. + [1]: https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations" diff --git a/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md b/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md index 5669216d2..cecd4f2ab 100644 --- a/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md +++ b/proofs/solidity-verifier/docs/audit/CODEGEN_ASSURANCE_DOSSIER.md @@ -37,7 +37,9 @@ manifest: | Rust toolchain | `rust-toolchain.toml` | | Cargo features | Exact feature list used to generate the verifier | | Solidity compiler | `solc 0.8.30+commit.73712a01` | -| Solc flags | `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` | +| Solc flags | `--bin --optimize --optimize-runs --via-ir --evm-version cancun --no-cbor-metadata` — `` is bytecode- and deployability-affecting (0.8.30 at `runs=100000` exceeds EIP-170) and MUST be recorded per artifact; default `200` (`SOLC_OPTIMIZE_RUNS`), IVC bench uses `1` | +| Solc binary | SHA-256-pinned by `scripts/install_pinned_solc.sh`; hashes recorded in `docs/reference/REPRODUCIBLE_BUILDS.md` | +| SRS provenance | SHA-256 of the SRS asset(s) plus ceremony reference; produced by `scripts/record_srs_provenance.sh`, recorded in `docs/reference/REPRODUCIBLE_BUILDS.md` | | Generated sources | Hashes of verifier, VK, and optional quotient evaluator sources | | Runtime bytecode | Runtime length and `keccak256` for each deployed artifact | | VK binding | VK digest plus external VK runtime length/codehash when split | @@ -56,12 +58,12 @@ line-for-line translation. | Checkpoint | Rust source of truth | Solidity/codegen owner | Evidence expected | | --- | --- | --- | --- | -| Parser and proof reads | `plonk/verifier.rs::{parse_trace,verify_algebraic_constraints}` | `src/codegen/protocol.rs`, `src/codegen/proof_layout.rs`, `templates/contracts/Halo2Verifier.sol` | Exact proof length, ABI head checks, per-section offsets, canonical scalar and G1 checks | -| Transcript challenges | `transcript/mod.rs`, `transcript/implementors.rs` | `src/transcript.rs`, `templates/contracts/Halo2Verifier.sol` | Rust/Solidity trace equality for VK, instances, commitments, and all challenges | -| Quotient identities | `plonk/mod.rs::partially_evaluate_identities` | `src/codegen/evaluator.rs`, `src/codegen/quotient/mod.rs`, `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` | Identity order manifest, quotient trace ids, VM validator, selector-fold trace coverage | -| Linearization | `plonk/linearization/verifier.rs::compute_linearization_commitment` | `src/codegen/generator.rs`, `src/codegen/pcs.rs` | Same `-nu_y(x)` scalar, selector buckets, quotient limb scalars, and commitment expansion | -| KZG multi-open | `poly/kzg/mod.rs::multi_prepare` | `src/codegen/pcs.rs` | Same dummy queries, point-set sort, `x1..x4`, `f_com`, `q_evals`, `pi`, final MSM | -| Final pairing | `poly/kzg/msm.rs::DualMSM::check` | `templates/contracts/Halo2Verifier.sol`, `src/codegen/pcs.rs` | Pairing precompile success, return size, and semantic result word checked | +| Parser and proof reads | `plonk/verifier.rs::{parse_trace,verify_algebraic_constraints}` | `src/lowering/protocol/mod.rs`, `src/lowering/abi/proof.rs`, `templates/contracts/Halo2Verifier.sol` | Exact proof length, ABI head checks, per-section offsets, canonical scalar and G1 checks | +| Transcript challenges | `transcript/mod.rs`, `transcript/implementors.rs` | `templates/partials/verifier/TranscriptProofParser.yul`, `src/lowering/abi/` (offsets), `templates/contracts/Halo2Verifier.sol` | Rust/Solidity trace equality for VK, instances, commitments, and all challenges | +| Quotient identities | `plonk/mod.rs::partially_evaluate_identities` | `src/lowering/quotient_numerator/` (planning, Yul emitter, VM), `src/lowering/quotient.rs`, `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` | Identity order manifest, quotient trace ids, VM validator, selector-fold trace coverage | +| Linearization | `plonk/linearization/verifier.rs::compute_linearization_commitment` | `src/lowering/quotient.rs`, `src/lowering/kzg/mod.rs` | Same `-nu_y(x)` scalar, selector buckets, quotient limb scalars, and commitment expansion | +| KZG multi-open | `poly/kzg/mod.rs::multi_prepare` | `src/lowering/kzg/mod.rs` | Same dummy queries, point-set sort, `x1..x4`, `f_com`, `q_evals`, `pi`, final MSM | +| Final pairing | `poly/kzg/msm.rs::DualMSM::check` | `templates/contracts/Halo2Verifier.sol`, `src/lowering/kzg/mod.rs` | Pairing precompile success, return size, and semantic result word checked | | Optional accumulator | Wrapper logic outside `plonk/verifier.rs` | `AccumulatorEncoding`, VK header, `templates/contracts/Halo2Verifier.sol` | Public-input schema validation and batched accumulator/KZG pairing equation | The bridge between Solidity calldata and Rust verifier objects is: diff --git a/proofs/solidity-verifier/docs/audit/FIXES_APPLIED_2026-08.md b/proofs/solidity-verifier/docs/audit/FIXES_APPLIED_2026-08.md new file mode 100644 index 000000000..aae9060f8 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/FIXES_APPLIED_2026-08.md @@ -0,0 +1,285 @@ +# Fixes applied — August 2026 verifier review + +Companion to `HALO2_VERIFIER_REVIEW.md`. This file records exactly what was changed, what was verified, and — importantly — **what the review found that these changes do not fix.** + +## 2026-08-12 follow-up: generator-side closures + +A second pass (with the workspace compilable) closed most of what the first +pass could only specify. Newly applied and validated: + +- **P1 / M-2 — exact precompile gas bounds.** Gas model in + `src/lowering/layout/mod.rs::gas` (EIP-2537 discount table + pairing + formula, EIP-2565 modexp), threaded through `GasTemplateConstants` and + emitted as `G1ADD_GAS` / `G1MSM_GAS_1PAIR` / `G1MSM_GAS_SMOKE` / + `PAIRING_GAS_2PAIR` / `MODEXP_GAS` / `ACC_RHS_MSM_GAS` constants. Every + precompile call site — templates, PCS emitter, and constructor probes — + now forwards the exact scheduled cost instead of `gas()`; the two + `quotientEvaluator` calls keep `gas()` deliberately (regular-call refund + semantics, codehash-pinned callee) with a comment. Decision on cap style: + exact schedule, no margin, per EIP-2537's DDoS-protection guarantee; + liveness caveat (upward repricing ⇒ regenerate + redeploy) documented at + the constants and in the spec §15. This deliberately reverses commit + `2b2bf49` ("Forward gas to EIP-2537 precompiles", old finding M-04): those + were hand-tuned literals, these are generated spec formulas, and the + constructor probes now fail-fast on repriced chains, which is what M-04 + actually needed. Verified: `eip2537_gas_schedule_matches_spec_vectors`, + `eip2537_calls_forward_exact_schedule_gas`, and the revm test + `malformed_proof_point_rejects_with_bounded_gas` — a malformed point now + costs no more than an honest verification (was 29.5M of a 30M limit). +- **P11 / H-1 (code half) — applied and repaired.** The patch as shipped + could not compile: it shadowed the `omega: U256` binding with an `Fq` and + referenced an undefined `srs_tau_is_consistent`. Landed in + `src/lowering/vk.rs` with the helper implemented as a Pippenger-MSM + Lagrange commitment of f(X)=X plus the pairing check + `e([τ]G1, G2) == e(G1, s_g2)`. Verified: + `srs_tau_binding_accepts_honest_params_and_rejects_foreign_s_g2` (honest + SRS passes; s=1, s'=2s, and wrong-domain omega all fail). +- **H-1 (non-code half) — SRS provenance recorded.** Ceremony citation + (github.com/midnightntwrk/midnight-trusted-setup), asset SHA-256s hashed + by the new `scripts/record_srs_provenance.sh` and verified byte-identical + against the ceremony's official `MIDNIGHT_SRS_CATALOG.md`; the gated test + `midnight_srs_assets_bind_s_g2_to_lagrange_tau` pairing-checks the real + 2p19/2p20 assets. All recorded in `REPRODUCIBLE_BUILDS.md` §"SRS + Provenance". +- **P7 / L-2 — q_eval_set comments.** Emitter now reports + "{m} evaluation term(s), {n} commitment term(s)" (identity commitments + excluded from n), so the comment matches the MSM pair count below it. The + structural asserts (`pair_idx == non_identity_terms` / + `== final_msm_terms`) plus a new whole-pairs assert guard the counts. +- **P13 / M-5 — solc hashes filled and script fixed.** Official sha256 for + linux-amd64 and macosx-amd64 recorded (see `REPRODUCIBLE_BUILDS.md`); + found and fixed a latent portability bug (`declare -A` fails on stock + macOS bash 3.2); end-to-end verified by a real download+hash+version run. + Rosetta mapping for Darwin-arm64 now documented in the script. +- **M-5 (remainder) — manifest + flags + provenance stamps.** New + `scripts/generate_artifact_manifest.sh` produces the §4 manifest rows per + fixture dump; `--optimize-runs` added to every recorded flag list + (REVIEW_PACKET, dossier, REPRODUCIBLE_BUILDS) with the + `runs=100000`-undeployable warning; the three provenance stamps were not + contradictory but unlabeled — each is now labeled, with the canonical + table in `REPRODUCIBLE_BUILDS.md` §"Provenance Identities". Still open + from the original M-5 list: removing the `SOLC_OPTIMIZE_RUNS` env + override from the reproducible path (kept, as the bench profiles rely on + it; the manifest records the effective value instead). +- **I-4 — stale `src/codegen/*` anchors.** All 58 references across 13 + files fixed or covered: living reviewer docs (dossier §2 semantic map, + plans, benchmarks, template comment) rewritten to the current + `src/lowering/*` tree; the historical audit records (`AUDIT.md`, + `AUDIT_FINDINGS.md`) carry a path-migration map at the top instead of + rewritten history. `AUDIT_FINDINGS.md` 2026-05-11 #4's evidence cell now + cites validators/tests that actually exist + (`validate_quotient_program` / `quotient_vm_safety_validator_rejects_malformed_programs`); + the previously cited names never landed. One review-side correction: I-4 + overreached on `QuotientNumeratorBlock.yul`, which does exist. +- **TA-1…TA-8 triaged.** Every TA item in `AUDIT.md` now carries a + `Status:` line (TA-1 Fixed — the quoted scalar-0 skip no longer exists; + TA-4 resolved/diagnostic-only; TA-5/TA-6 fixed by `460a666`; TA-2/TA-3 + open, cross-referenced to the deferred M-4 digest decision; TA-7 + fail-closed semantics now specified in the spec §12.7; TA-8 integration + guidance). +- **I-2 (partial).** The generated opcode-summary comment is now rendered + from the same `program.op_usage` predicates that gate the interpreter's + case arms, so each artifact documents exactly its own opcodes; the + `G1_IDENTITY_MPTR` comment no longer references nonexistent `mload`s. + +**New finding discovered and fixed during this pass — feature-unification +challenge-truncation mismatch.** The `midnight-aggregation` dependency in +`Cargo.toml` unconditionally enabled `truncated-challenges`, which Cargo +feature unification forced onto `midnight-proofs` in EVERY build of this +crate. Consequence: the prover always truncated challenges, but a verifier +generated with `--features evm` (without this crate's own +`truncated-challenges` feature) did not mirror it — exactly the "wrong +setting on either side silently produces invalid pairings" hazard the +feature's own doc comment warns about. Every `evm`-only fixture test +(`rsa_signature`, `sha_preimage`, `hybrid_mt`) had been failing with valid +proofs reverting mid-PCS; bisect showed the breakage entered with the +workspace merge that introduced the aggregation dependency, long before the +2026-08 review. Fixed by removing the dependency-level feature so truncation +flows only through this crate's `truncated-challenges` feature; both +`--features evm` and `--features evm,truncated-challenges` now agree +end-to-end and all fixture tests pass in both modes. The committed +`target/*-dump` artifacts are canonically rendered with +`evm,truncated-challenges` (they contain the truncation code paths). + +Deliberately not addressed, by explicit decision: **M-4** (`vk_digest` +coverage — protocol change affecting the prover) and anything outside +`proofs/solidity-verifier/`. Remaining open from the tables below: P4, P8, +P9, P10, P12, P14, L-4, L-9, I-6, I-7. + +## 2026-08-13 follow-up: Low/Informational batch closed + +The remaining P-series patches and L/I findings were applied in a third pass +(commits: planner asserts, runtime hardening, provenance/digest, docs): + +- **P9 (L-7)** — planner asserts tie the batch_invert scratch capacity and + the Lagrange run length to the template's own expression; overflow (which + corrupted silently) now refuses to plan. Region-shape tests included. +- **P8 (L-5)** — the user-challenge window is asserted to cover the phase + sum of squeezed challenges and to end at or before THETA_MPTR; an + undersized window refuses to plan (positive + should-panic tests). +- **L-2 (assert half)** — `queries()` asserts every identity-pinned + commitment originates from the committed-instance column, the exact + justification for the MSM omission. +- **I-6** — both latent divergences are now plan-time assertions: the + permutation z ordering is re-derived under the upstream (Cur/Next then + reversed Last) order and the intermediate-set structures compared; the + column-based vs query-based fixed-eval counts are asserted equal. +- **P12 (L-6)** — the runtime quotient VM clamps every decoded + memory-pointer operand against the coarse union of the planned read + windows (shared source of truth with the build-time validator), clamps + FOLD_SELECTOR's bucket index and y-power gap, and guards stack pops + against balanced underflow. +- **P4 (L-3)** — seven declared custom errors cover the verify path + (constructor probes keep bare reverts by scoped decision); selectors + pinned against keccak of the signatures and decoded end-to-end in revm. +- **L-4** — decision: keep the exact `calldatasize` pin; the ERC-2771 / + calldata-appending incompatibility is now documented in `verifyProof`'s + NatSpec (typed BadCalldataShape revert). +- **I-7** — `vk_digest` joined the accumulator batching randomizer's + preimage (verifier-local; no prover interaction). +- **P10 (L-8)** — every render carries `BUILD_ID` (feature profile, + vk_digest, VK codehash, SRS fingerprint keccak(n||G2||s_g2||[tau]G1), + optional deployment provenance tag via `RenderOptions::provenance`). +- **P14 (L-1)** — `tests/template_digest.rs` pins a keccak of the sorted + `templates/` tree in default CI, closing the committed-fixture drift gap. + The deeper L-1 legs (expression front-end certification, native-kernel + differential) remain future work. +- **L-9 / I-5** — `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md` + states the wrapper obligations as requirements, adds the incident/migration + playbook keyed on BUILD_ID, and records the transcript domain-separation gap + (I-5) as accepted — it cannot be fixed verifier-side without breaking prover + compatibility. +- **I-2 leftovers / I-3** — smoke-window comment corrected (creation-frame + memory cannot pre-expand the runtime frame), `validate_public_accumulator`'s + shape-dependent `r` parameter documented, the unreachable identity-flag + branch labeled unreachable-by-construction. + +Still open after this batch: the deeper L-1 certification legs, M-4 +(excluded by owner decision), and the outer single-H recorded-hash refresh. + +## Short answer: no, this does not address all the findings + +Of the 23 findings, **4 are closed or substantially closed by the changes below.** The rest fall into three groups: + +- **8 need generator logic** (Rust changes plus values computed at codegen time). They are fully specified in Part V of the review with exact insertion points, but were not written, because the generator cannot be compiled from this session — `solidity-verifier/Cargo.toml` depends on `../../curves`, `../../circuits` and `../../zk_stdlib`, which are outside the connected folder. Writing Rust into a workspace that cannot be `cargo check`ed would risk breaking your build to no benefit. +- **1 is patch-ready but unverified** (`patches/P11_srs_binding.patch`) for the same reason. +- **9 are not code problems.** They need data or decisions only you hold: the ceremony reference, the SRS hash, the artifact manifest, the incident-response policy, an accepted-risk sign-off on the truncated-challenge security level. No patch can close those. + +--- + +## Applied and validated + +Verified by compiling the rendered output with the pinned toolchain (solc 0.8.30, `--via-ir --optimize --optimize-runs=1 --evm-version cancun`, CBOR off) and running it under `revm 19` / `SpecId::PRAGUE` against the `moonlight-wrap` fixture plus 40 adversarial mutations. + +| Patch | Finding | File | +| --- | --- | --- | +| **P2** | M-1 | `templates/contracts/Halo2Verifier.sol`, `templates/partials/verifier/PrecompileSmoke.sol` | +| **P3** | M-1 | all three `templates/contracts/*.sol` | +| **P5** | M-3 | `templates/partials/verifier/PrecompileSmoke.sol` | +| **P6** | I-1 | `templates/contracts/Halo2Verifier.sol` | + +**P2 — free-memory-pointer guard.** `if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) }` at the top of the main assembly block, and the equivalent in `require_eip2537_precompiles` (which runs in the *creation* frame that `compiled_memoryguard_does_not_overlap_generated_layout` does not inspect). The invariant is now enforced by the deployed bytecode rather than by a test in this repository that an integrator recompiling the `.sol` will never run. + +**P3 — pragma pinned to `0.8.30`.** Not cosmetic. Measured: + +``` +solc 0.8.24 runs=1 -> 29,567 B runtime -> HALT CreateContractSizeLimit +solc 0.8.30 runs=100000 -> 29,836 B runtime -> HALT CreateContractSizeLimit +solc 0.8.30 runs=1 -> 21,286 B runtime -> deploys +``` + +`^0.8.24` advertised a compiler that cannot produce a deployable contract. The spill reservation also moves with the version (`0x8c0` on 0.8.24, `0x8e0` on 0.8.26+), which is the concrete reason the P2 guard is not theoretical. + +**P5 — four known-answer precompile probes.** The existing smoke test hardened `G1ADD` with a known-answer vector and gave a good explanation of why identity inputs prove nothing — then tested `G1MSM` and `PAIRING_CHECK` with identity inputs only. Those are the two precompiles the verifier's security actually rests on: `G1MSM` is the curve/subgroup validator for every absorbed proof commitment, and `PAIRING_CHECK` is the sole accept gate. Added: + +- **(a)** `G1MSM([2]·G) == 2G` — a stub that echoes zeros fails this. +- **(b)** `G1MSM` **must reject** `(4, y)` — a point satisfying `y² = x³ + 4` over `Fp` that is provably *not* in the r-order subgroup (verified off-chain: `r·P ≠ O`). This is the one property the entire deferred-validation strategy depends on and the one no other probe exercised. Gas is deliberately bounded to 200,000: a rejecting precompile consumes everything forwarded to it, so an unbounded `gas()` here would burn 63/64 of the deployment gas. +- **(c)** `e(G, G₂)·e(−G, G₂) == 1`. +- **(d)** `e(G, G₂)·e(G, G₂) != 1` — catches a `0x0f` that always returns 1, which would otherwise accept every proof. + +This closes **M-3** and addresses `AUDIT.md` TA-6, which was still un-triaged (no `Status:` line). TA-6 should now be marked resolved and its severity reconsidered — with the accept path resting entirely on `0x0f`, Low was too low. + +**P6 — comments that contradicted the code.** The NatSpec claimed "generated scratch starts at `0x80`" while `TRANSCRIPT_MPTR = 0x1000`. That was the exact stale belief TA-5 identified as the hazard, hardcoded in the template so every render shipped it. Also corrected the `docs/MEMORY_LAYOUT.md` path to `docs/architecture/MEMORY_LAYOUT.md`. + +### Measured cost + +| | baseline | patched | delta | +| --- | --- | --- | --- | +| verifier runtime | 21,286 B | 21,299 B | **+13 B** | +| deployment gas | 5,330,806 | 5,766,768 | +435,962 (one-time) | +| valid-proof gas | 1,279,482 | 1,279,513 | **+31** | +| 40-case test suite | 1 accept / 39 reject | identical | — | +| non-Prague deploy (CANCUN/SHANGHAI/MERGE) | reverts | reverts | — | +| mutated-VK / EOA-VK deploy | reverts | reverts | — | + +The strengthened probes cost **nothing** at verify time — they run only in the constructor. That deployment succeeds is itself the validation: all four new known-answer probes pass against a correct EIP-2537 implementation, so the constants are right. + +--- + +## Applied, syntax-checked only + +**P13 — `scripts/install_pinned_solc.sh`** now verifies the downloaded compiler by SHA-256 and fails closed. `bash -n` passes. + +⚠️ **Action required:** the hashes are `TODO-fill-from-list.json`. Fetch them from `https://binaries.soliditylang.org/{linux-amd64,macosx-amd64}/list.json` and fill them in — until then the script refuses to install, which is the intended fail-closed behaviour but will block a fresh checkout. Also still open: `Darwin-arm64` maps to `macosx-amd64`, so Apple Silicon runs an x86 binary under Rosetta, which is a different binary from CI. + +--- + +## Patch supplied, not applied + +**`patches/P11_srs_binding.patch`** — the highest-severity finding (**H-1**). Adds two assertions to `src/lowering/vk.rs`: + +1. `params.g2() == G2Affine::generator()` — the G1 side is already defended this way; the G2 side was not. +2. A pairing consistency check that `s_g2` corresponds to the same τ that produced `g_lagrange`, by committing `f(X) = X` in the Lagrange basis to obtain `[τ]G1` and checking `e([τ]G1, G₂) == e(G1, s_g2)`. + +**Not applied because it cannot be compiled from here.** It will likely need small adjustments — the pairing helper's import path, and the exact `Fq`/domain accessor names. Please `cargo check` before committing. The `srs_tau_is_consistent` call in the patch is a placeholder for whatever pairing helper your curve crate exposes. + +This patch closes the *checkable* half of H-1. The other half is not code: name the ceremony, record the SRS SHA-256, and publish `NEG_S_G2_BASE` next to the ceremony's published τ point so a third party can verify the negation independently. `CODEGEN_ASSURANCE_DOSSIER.md:46` already lists the SRS record as required; nothing currently produces it. + +--- + +## Specified but not written — needs generator work + +Each is fully specified in Part V of the review, with the insertion point named. + +| Patch | Finding | Why it needs the generator | +| --- | --- | --- | +| **P1** | M-2 | Needs an EIP-2537 gas model in `plan.rs` to emit per-call-site caps. **This is the one with real operational impact** — a single off-curve byte currently burns ~98.5% of whatever gas limit you supply (29.5M of 30M). | +| **P4** | L-3 | Custom errors touch ~41 revert sites, some template-resident and some emitted from `src/lowering/kzg/mod.rs`; a partial application would leave the taxonomy inconsistent. | +| **P7** | L-2 | Codegen assertion that MSM `x1` exponents omit exactly the identity commitments, plus fixing the emitted comment that says "43 commitment(s)" and emits 42. | +| **P8** | L-5 | `CHALLENGE_MPTR` must get its own window in the memory planner. | +| **P9** | L-7 | Planner assertion on `batch_invert` scratch capacity. Note this one currently fails **silently** — the modexp still succeeds, so nothing reverts. | +| **P10** | L-8 | `BUILD_ID` needs generator commit, feature list and SRS hash plumbed into the render context. | +| **P12** | L-6 | Quotient-VM operand clamps need new template variables for the window bounds. | +| **P14** | L-1 | A `templates/` tree digest test. Cheapest closure of the codegen-trust gap, and needs no SRS, so it runs in default CI. | + +--- + +## Not fixable by patch — needs your data or a decision + +| Finding | What is needed | +| --- | --- | +| **H-1** (remainder) | Ceremony name and transcript URL; SRS file SHA-256; publish `NEG_S_G2_BASE` against the ceremony's τ. | +| **M-4** | Redefining `vk_digest` to cover the SRS points, quotient program, accumulator schema and feature profile is a protocol change affecting the prover — a design decision, not a patch. | +| **M-5** (remainder) | Fill the `REVIEW_PACKET.md` §4 manifest (currently `fill per artifact` in all 11 rows); reconcile the three conflicting provenance stamps (`3fb6d84` / `a096e71…` / `53dc872…`); add `--optimize-runs` to the recorded flag set and remove the `SOLC_OPTIMIZE_RUNS` env override from the reproducible path; ship `REPRODUCIBLE_BUILDS.md`. | +| **L-4** | Decide: relax the `calldatasize` pin to `>=`, or document that ERC-2771 forwarders cannot call this verifier. Both are defensible; the other four pins already carry the security property. | +| **L-9** | Write the incident-response and migration section, and state the wrapper obligations as requirements: replaceable verifier address, wrapper-held pause, and binding `block.chainid` + wrapper address into the statement. | +| **I-4** | Rewrite the `src/codegen/*` anchors in `CODEGEN_ASSURANCE_DOSSIER.md` and `AUDIT_FINDINGS.md` to the current `src/lowering/*` tree. Until then the ~20 findings marked "Fixed with named tests" in the 2026-05-11 addendum cannot be re-verified by a reviewer. | +| **I-2, I-3, I-6, I-7** | Cleanup and assertions; low value, no urgency. | + +--- + +## Re-render required + +The changes are to **templates**, so the committed fixtures under `fixtures/` and `deployments/` are now stale relative to them. Before relying on any of this: + +1. Re-render the fixtures (see each `fixtures/*/README.md` — needs the SRS and a Moonlight checkout). +2. Update the source-commit stamps in those READMEs. +3. Re-run the replay tests. Note that `tests/ivc_accumulator_replay.rs` compiles the **committed** `.sol`, not fresh template output, so it will keep passing either way — which is finding **L-1**, and the reason P14 matters. + +## Suggested next steps + +1. `cargo check` and commit `patches/P11_srs_binding.patch` — highest severity, build-time only, no on-chain change. +2. Fill the solc SHA-256 values so P13 stops blocking fresh checkouts. +3. P1 — the gas cap. It is the finding with day-one operational impact for any relayer or batching wrapper. +4. P14 — one test, no SRS needed, closes the widest assurance gap for the least work. +5. Fill the artifact manifest and reconcile the provenance stamps (M-5). diff --git a/proofs/solidity-verifier/docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md b/proofs/solidity-verifier/docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md new file mode 100644 index 000000000..e392efbaf --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md @@ -0,0 +1,1400 @@ +# Halo2 BLS12-381 Solidity Verifier — Architecture Review, Conformance Check and Security Audit + +**Artefacts under review** + +| File | SHA-256 | Size | +| --- | --- | --- | +| `Halo2Verifier.sol` | `3861a403f7319e767610fc71a3ff5500b2c14e787a68703188d2f2629928e756` | 213,670 B / 3,682 lines | +| `Halo2VerifyingKey.sol` | `ec94cabe5691703e240b116c59bc2e7c83cbdefe2e4d02dbd64020d6a0294f19` | 78,098 B / 692 lines | + +Both files are **byte-identical** to `midfall/proofs/solidity-verifier/fixtures/moonlight-wrap/`. This is the `AccumulatorEncoding::point_pair` replay fixture, rendered by Moonlight's `wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs` at solidity-verifier commit `3fb6d84`. + +**Reference implementation:** `midfall/proofs` (`src/plonk/*`, `src/poly/kzg/*`, `src/transcript/*`) and the generator at `midfall/proofs/solidity-verifier`. + +**Method.** Four independent passes were run and reconciled: a line-by-line conformance diff against the Rust verifier; a cryptographic/soundness pass; an EVM/implementation pass; and an operational/key-management/supply-chain pass. Findings were then **executed**: both contracts were compiled with solc 0.8.30 (`--via-ir --optimize --optimize-runs=1 --evm-version cancun`, CBOR metadata stripped) and run under `revm 19` with `SpecId::PRAGUE` and the `blst` BLS12-381 precompiles, against the fixture calldata and 40 adversarial mutations. + +--- + +## Executive summary + +**The verifier is sound as rendered.** No Critical or High soundness break was found in the contract. Every phase that could be checked against staged reference code — transcript schedule and encodings, challenge order, instance handling, all four identity families, the y-batching algebra, linearization and quotient reconstruction, the multi-open reduction, and the pairing equation including which side carries the negation — conforms to `midfall/proofs`. The domain constants, the permutation `DELTA`, the base points and the VK codehash were all recomputed independently and match. + +The code is also, by a wide margin, better defended than a typical generated verifier. It re-pins the VK by `extcodehash` **on every proof**, not just at construction. It pins both ABI head words, the proof length, the instance count and `calldatasize()` before touching any data-dependent calldata. It range-checks every scalar against `r` with `lt`, not `mod`. It rejects non-canonical EIP-2537 padding rather than normalising it (which would create transcript aliases). It routes every prover-supplied G1 through a subgroup-checking precompile. It compares the pairing result `== 1` rather than masking the low bit. And it asserts structural post-conditions on its own quotient interpreter (`q_pc == q_end`, empty stack, no live operand). + +**The risk is not in the Solidity — it is around it.** The highest-severity finding is that `NEG_S_G2_BASE`, the trusted-setup element on which all soundness rests, is taken verbatim from whatever SRS the generator was handed, with no ceremony binding, no SRS hash, and no build-time consistency check — even though the generator *does* defend the G1 base with exactly such a check. Below that sit a cluster of build-reproducibility and deployment-assurance gaps, a compiler-configuration dependency that is not self-enforcing in the shipped artefact, and a weak deployment-time probe of precisely the two precompiles that decide acceptance. + +One new, empirically-demonstrated issue: **a single off-curve point in the proof causes the verifier to consume ≈98.5% of whatever gas limit is supplied** (29.5M of a 30M limit, versus 316k for a structural rejection and 1.28M for a valid proof), because a failing EIP-2537 precompile consumes all gas forwarded to it. This is a griefing vector for any relayer, paymaster or batching wrapper. + +### Findings at a glance + +| ID | Severity | Title | +| --- | --- | --- | +| **H-1** | High | Trusted-setup element `NEG_S_G2_BASE` is unverifiable: no ceremony binding, no SRS pin, no build-time consistency check | +| **M-1** | Medium | `assembly ("memory-safe")` is factually false; the invariant that protects it is enforced only by an external test, under a floating pragma | +| **M-2** | Medium | Malformed curve points burn ~63/64 of the supplied gas instead of reverting cheaply (demonstrated) | +| **M-3** | Medium | Deployment probe is weakest exactly where acceptance is decided: `PAIRING_CHECK` and `G1MSM` are tested only with identity inputs | +| **M-4** | Medium | `vk_digest` does not cover the SRS points, the quotient program, the accumulator schema or the feature profile — the team's own TA-2 remediation is still open | +| **M-5** | Medium | Build is not reproducible as documented: three conflicting provenance stamps, empty artefact manifest, `--optimize-runs` unrecorded and environment-overridable, solc pinned by version string not hash | +| **L-1** | Low | Codegen certification covers the emitter→reference leg only; the Yul VM and native kernels are fixture-sampled, and the replay test recompiles committed fixtures | +| **L-2** | Low | Committed-instance commitment hard-wired to the identity; one MSM term silently omitted while the emitted comment claims otherwise | +| **L-3** | Low | 41 bare `revert(0, 0)` sites; `verifyProof` can never return `false` despite its signature | +| **L-4** | Low | Exact-`calldatasize` pin makes the verifier uncallable through ERC-2771 forwarders and similar calldata-appending relayers | +| **L-5** | Low | `CHALLENGE_MPTR` aliases `THETA_MPTR` — inert here, silent corruption for any circuit with user-phase challenges | +| **L-6** | Low | Quotient-VM operands are unvalidated raw memory pointers; safety rests entirely on codehash pinning plus emitter correctness | +| **L-7** | Low | Zero-slack memory adjacencies; `batch_invert` scratch ends exactly at `LAGRANGE_DENOMS_MPTR` and would corrupt silently, without reverting | +| **L-8** | Low | No on-chain provenance: no build id, no feature profile, CBOR metadata stripped | +| **L-9** | Low | No incident-response or migration story; no domain binding, so a valid proof replays across every chain and deployment | +| **I-1…I-7** | Info | Comments contradicting constants, dead code and constants, unreachable defensive branch, stale audit-doc anchors, missing transcript domain separation, two latent codegen divergences, unbound batching randomiser | + +--- + +# Part I — How it works + +## 1.1 Two contracts, and why + +The system is a **circuit-specialised** verifier: the proof layout, memory map, quotient program and every constant are generated for exactly one `VerifyingKey>`. It is not a generic Halo2 verifier and cannot verify a proof for any other circuit. + +It ships as two contracts because the verifying key is too large to sit comfortably inside the verifier's own bytecode: + +- **`Halo2VerifyingKey`** — a *data* contract. Its constructor writes a byte-blob into memory and `return`s it as runtime code. There is no executable logic in the deployed runtime at all. +- **`Halo2Verifier`** — the logic. A thin Solidity shell (`AUTHORIZED_VK`, a constructor, and `verifyProof`) wrapped around one very large `assembly` block that does the entire verification. + +The verifier binds to its key by **address, byte length and codehash** (`EXPECTED_VK_LENGTH = 17025`, `EXPECTED_VK_CODEHASH = 0xe68d8936…cf52`), and re-checks that binding before every single proof, not just at deployment. + +## 1.2 The VK contract: data as code + +```solidity +constructor() { + assembly { + let runtime := 0x80 + let payload := add(runtime, 0x01) + mstore8(runtime, 0xfe) // INVALID opcode + mstore(add(payload, 0x0000), 0x56c0…) // vk_digest + …532 words total… + return(runtime, 0x4281) // 17025 bytes + } +} +``` + +Byte 0 is an unconditional `INVALID` (`0xfe`), so a direct call to the VK address cannot execute the payload as code. The verifier copies from byte 1 onward: + +```yul +extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) // 17024 bytes +``` + +I reconstructed the runtime from the 532 `mstore`s, prepended the `0xfe`, and confirmed `keccak256` equals `EXPECTED_VK_CODEHASH` and the length equals 17025 — so the pinned hash covers the `INVALID` prefix, and the two contracts are mutually consistent. Because the runtime is pure returned data, **this codehash is compiler- and optimiser-independent**, which is a genuinely good design property: the VK half of the system is trivially reproducible even though the verifier half is not (see M-5). + +The payload layout, in 32-byte words from `VK_MPTR = 0x3680`: + +| Words | Content | Value in this artefact | +| --- | --- | --- | +| 0 | `vk_digest` (Blake2b-512 over the pinned constraint system, reduced) | `0x56c0824f…4f66` | +| 1 | `num_instances` | 19 | +| 2 | `k` (log₂ domain size) | 20 | +| 3 | `n_inv` | 1/2²⁰ mod r | +| 4–6 | `omega`, `omega_inv`, `omega_inv_to_l` | order-2²⁰ root; ω⁻¹⁰ | +| 7–10 | `has_accumulator`, `acc_offset`, `num_acc_limbs`, `num_acc_limb_bits` | 1, 11, 7, 56 | +| 11–14 | `G1_BASE` (4 words, EIP-2537 padded) | canonical BLS12-381 G1 generator | +| 15–22 | `G2_BASE` (8 words) | canonical G2 generator | +| 23–30 | `NEG_S_G2_BASE` (8 words) | −[s]G₂ from the trusted setup | +| 31… | quotient VM constant pool (178 words) + packed program (143 words, `0x11cf` bytes) | | +| … | 27 fixed commitments, then 18 permutation commitments (4 words each) | | + +The header ends exactly at `0x7900 = CHALLENGE_MPTR` — the VK region and the challenge region abut with zero slack. + +I verified numerically: `n_inv · 2²⁰ ≡ 1 (mod r)`; `omega` has exact multiplicative order 2²⁰; `omega · omega_inv ≡ 1`; `omega_inv_to_l = omega_inv¹⁰`, consistent with `|rotation_last| = 10` and `blinding_factors = 9`; `G1_BASE` and `G2_BASE` are the canonical generators; `NEG_S_G2_BASE` is on the twist, in the r-order subgroup, and is not ±G₂; all 45 VK G1 commitments are on-curve and in the r-subgroup; and none of the 10 simple-selector commitments is the point at infinity (an identity selector commitment would have silently zeroed every identity in its bucket). + +## 1.3 The verifier's memory map + +The single most unusual design decision: the generated code **does not use Solidity's free-memory pointer at all**. Every address is an absolute constant baked in at codegen time, starting at `0x1000`. + +``` +0x0000–0x005f Solidity scratch (never written) +0x0040 free-memory pointer (never written, never read) +0x0080–0x0fff solc's via-IR stack-spill reservation +0x1000 TRANSCRIPT_MPTR = RETURN_MPTR = QUOTIENT_RETURN_MPTR + ↳ streaming Keccak buffer, peaks at 0x1ce0 + ↳ later reused for PCS scaling, the acc-batch preimage, + the ec_pairing frame, and finally the returned word +0x3580–0x363f scalar_inv modexp frame +0x3680–0x78ff VK payload (copied by extcodecopy) +0x7900–0x7a3f challenges: theta β γ trash y x x1 x2 x3 x4 +0x7a40–0x7f7f f_com, pi, acc lhs/rhs, Lagrange scratch, quotient, pairing slots +0x7f80–0x931f rotation points, x1 powers, q_eval sets +0x9480–0xa13f REVERSED_EVALS (102 spilled proof evaluations) +0xa140–0xb13f proof G1 commitments, by category +0xb140 SELECTOR_ACC_MPTR = BATCH_INV_SCRATCH_MPTR +0xb280–0xe33f the fused 78-pair final G1MSM input (0x30c0 bytes) +0xe340 end +``` + +Three addresses are deliberately aliased (`0x1000` three ways, `0xb140` two ways). I traced the write map in program order and confirmed all of them are **lifetime-disjoint reuse, not collisions**: the transcript is dead by line 1377, long before `0x1000` is next touched at line 3570; `batch_invert` runs exactly once (line 1431) and finishes before the selector buckets are zeroed at line 1612. + +The strategy is fast — an absolute `mload` costs 3 gas and needs no pointer arithmetic — and it is why this verifier lands at ~1.28M gas. It is also the source of finding **M-1**: the annotation `assembly ("memory-safe")` that unlocks solc's stack-to-memory mover is, on its own terms, false, and the invariant that keeps it safe (`solc's spill reservation < 0x1000`) is checked only by a test in the generator repo, never in the shipped bytecode. + +## 1.4 The verification flow + +`verifyProof(bytes proof, uint256[] instances)` — selector `0x1e8e1e13`. For this circuit: proof = 7,776 bytes, instances = 19 words, total calldata = 8,516 bytes exactly. + +**Phase 0 — envelope.** Before touching anything data-dependent, four independent pins: + +```yul +calldataload(0x04) == 0x40 // proof head +calldataload(0x24) == 0x1ec0 // instances head +calldataload(0x44) == 0x1e60 // proof length +calldataload(0x1ec4) == 19 // instance count +calldatasize() == 0x2144 // no missing or trailing bytes +``` + +This closes the classic hand-rolled-parser hole where an attacker supplies arbitrary ABI offsets and relocates the instance array. All five were confirmed by mutation testing (Appendix A, N23–N30, D-block). + +**Phase 1 — VK load.** `extcodesize` and `extcodehash` are both re-checked, then `extcodecopy` from byte 1. Six header words are cross-checked against constants baked into the verifier (`19, 20, 1, 11, 7, 56`). + +**Phase 2 — accumulator pre-validation.** Runs *before* the transcript, so malformed accumulator public inputs cannot influence challenge derivation. Described in §1.6. + +**Phase 3 — transcript.** A streaming Keccak buffer. `common_word` appends a 32-byte big-endian word; `common_uncompressed_g1` validates and appends 128 bytes; `squeeze_to` hashes the buffer, **reseeds the buffer with the digest**, and samples `digest mod r`. That reseed is exactly equivalent to the Rust `squeeze()`, which finalises a clone and re-inits a fresh `Keccak256` over the output. + +The absorb/squeeze schedule, verified byte-for-byte against `src/plonk/verifier.rs` and `src/poly/kzg/mod.rs`: + +``` +vk_digest → committed_pi (128 zero bytes) → 19 → 19 instances +→ 15 advice → [θ] → 2 lookup-m → [β] [γ] → 6 perm-Z +→ (helper + acc) ×2 → [trash] → 1 trashcan → [y] +→ 4 quotient limbs → [x] → 102 evaluations → [x1] [x2] +→ f_com → [x3, truncated to 128 bits] → 5 q_evals → [x4] → pi +``` + +Every prover-controlled value is absorbed strictly before any challenge that depends on it. `pi` is last and no challenge depends on it. + +Two validation policies matter here: + +- **Scalars** (19 instances, 102 evaluations, 5 q_evals) are checked `lt(v, r)` — a strict canonicality check, so `v = r` is rejected, not silently reduced. Non-canonical encodings would otherwise create transcript aliases. +- **G1 points** get their EIP-2537 padding checked (top 16 bytes of each `_hi` word must be zero) and each coordinate bounded by `p−1`, but **no on-curve or subgroup check here**. Validity is deferred: the code relies on every absorbed point later reaching an EIP-2537 `G1MSM` or `PAIRING_CHECK`, both of which do perform subgroup checks. That is a *codegen-time* invariant (`ProtocolPlan::validate`), and it is why the malformed-point gas behaviour in M-2 exists. + +**Phase 4 — Lagrange.** One 30-element batch inversion computes `x−ω^i` inverses plus `(xⁿ−1)⁻¹`; from these come `l_last` (index 0), `l_blind` (Σ indices 1–9), `l_0` (index 10) and `L_0…L_18` (indices 10–28) for the instance evaluation. If `x` lands on the domain, the batch product is zero, `batch_invert` returns 0, and the verifier reverts — so the `xⁿ = 1` degeneracy fails closed. + +**Phase 5 — quotient identity.** See §1.5. + +**Phase 6 — PCS.** Halo2's multipoint reduction (GWC-style with r-polynomial interpolation, not SHPLONK). 5 rotation sets of cardinality 1, 2, 2, 3, 3 over rotations {−10, −1, 0, +1}; batching by truncated powers of `x1` within a set and `x4` across sets; the whole commitment side collapses into **one fused 78-pair `G1MSM`**. + +**Phase 7 — pairing.** §1.7. + +## 1.5 The quotient identity — a bytecode interpreter in Yul + +The interesting engineering choice. Rather than emitting ~49 straight-line polynomial identities as Yul (which blows past the 24KB contract limit), the generator compiles them to a small stack-machine bytecode, stores that program **inside the VK payload**, and ships an interpreter in the verifier. + +Consequences: +- The program is covered by `EXPECTED_VK_CODEHASH`, so it cannot be tampered with post-deployment. +- The verifier avoids thousands of `PUSH32`/`mstore` immediates. +- The interpreter is ~1,500 lines of Yul with 11 implemented opcodes, including two "native kernels" (`NATIVE_PERMUTATION 0x19`, `NATIVE_LOOKUP 0x1f`) that run the permutation and LogUp arguments as hand-written Yul rather than interpreted arithmetic. + +The 49 identities are `y`-batched by Horner: `A := A·y + eval_j`, so identity *j* receives coefficient `y^(48−j)`, matching the Rust reverse fold in `linearization/verifier.rs`. Ten "simple selector" buckets accumulate separately and are multiplied into the commitment side at `x1^42`, which is how selector-compressed gates are handled. + +The interpreter asserts three structural post-conditions before proceeding: + +```yul +q_pc == q_end // consumed exactly the program +q_has_top == 0 // no live expression left +q_sp == 0xb8e0 // stack pointer restored +``` + +An unknown opcode hits `default { revert(0, 0) }`. These are good defences; the residual concern is **L-6** (operands are raw memory pointers with no bound check). + +## 1.6 The accumulator — recursive-proof handling + +`has_accumulator = 1`, `acc_offset = 11`: public inputs 11–18 encode **two G1 points**, each as 2 coordinates × 2 packed words, with each 381-bit `Fp` coordinate carried as 7 limbs of 56 bits packed 4-per-word. + +The codec is a shifted one: an encoded value `c` decodes to `c + 1`, and encoded `p−1` decodes to `0`. The point at infinity gets a single canonical encoding — `x = p−1 + 2⁵⁶` (the identity flag), `y = p−1` — checked by exact 4-word equality against pinned constants. + +The decoder is carefully written and I could not break it: + +- `check_acc_coord_packing` bounds packed word 0 below `2²²⁴` and word 1 below `2¹⁶⁸`, making the limb split a **bijection** — no two encodings of the same coordinate. +- The reconstructed coordinate is bounded by `p−1`, and `hi < 2¹²⁸` (the EIP-2537 pad rule). +- A decoded `(0, 0)` outside the sentinel path is explicitly rejected, so affine infinity has exactly one accepted encoding. +- `PACKED_0_WITH_ID_FLAG − PACKED_0 = 2⁵⁶` exactly — no carry into the neighbouring limb. +- The `sub(packed, first_adjust)` cannot underflow: the identity probe only runs behind a `calldataload(src) >= base` prefilter. + +Both decoded points are then forced through `G1MSM` with scalar 1 — not to compute anything, but because **the precompile is the on-curve/subgroup validator**. The comment says so explicitly, and the code does it even for identity points and unit scalars, which is exactly right: skipping the call would let a malformed non-identity point hide behind a zero scalar. + +The accumulator's own pairing equation `e(acc_rhs, G₂) = e(acc_lhs, [s]G₂)` is then folded into the KZG pairing by **randomised batching** rather than naive multiplication (which would let two false equations cancel): + +```yul +alpha = keccak256("pairing-batch-acc-kzg" ‖ kzg_rhs ‖ kzg_lhs ‖ acc_rhs ‖ acc_lhs) mod r +if alpha == 0 { alpha = 1 } +PAIRING_RHS += alpha · ACC_RHS +PAIRING_LHS += alpha · ACC_LHS +``` + +`alpha` is drawn only after all four G1 points are final, so if either equation is false the combined one holds for at most one `alpha` in `Fr` — a ~2⁻²⁵⁵ forgery probability. The zero-draw guard prevents the accumulator equation from being accidentally dropped. This is correct. + +## 1.7 The final pairing + +``` +e(final_com − v·G + x3·π, G₂) · e(π, −[s]G₂) = 1 +``` + +which rearranges to the standard KZG single-opening identity `e(C − vG + x3·π, G₂) = e(π, [s]G₂)`. The Rust reference negates the G₂ *generator* instead of `s·G₂`; algebraically the same equation, and the generated comment (lines 3653–3665) explains the LHS/RHS naming inversion honestly. + +`ec_pairing` checks three things — staticcall success, `returndatasize() == 0x20`, and `mload(scratch) == 1` (strict equality, not a low-bit mask) — and reverts on entry if `success` is already false, so no path hands control back to a caller that would report success for an unverified proof. + +--- + +# Part II — Conformance against `midfall/proofs` + +Every phase of the Rust verifier was diffed against the Solidity. The recovered circuit shape, derived from the artefacts and cross-checked for internal consistency: `k=20`, `cs_degree=5`, `blinding_factors=9`, `rotation_last=−10`, 15 advice columns (1 phase, 0 user challenges), 27 fixed columns of which 10 are simple selectors, 18 permutation columns → 6 sets of `chunk_len=3`, 2 LogUp lookups × 1 chunk, 1 trashcan, 4 quotient limbs, 2 instance columns (1 committed + 1 non-committed with 19 public inputs), 49 quotient identities (29 gate + 13 permutation + 6 lookup + 1 trash), 102 evaluations, 34 proof G1s, 5 KZG point sets, 78 final-MSM terms. Feature profile implied: `keccak-transcript` + `committed-instances` + `truncated-challenges` ON; `single-h-commitment` and `fewer-point-sets` OFF. + +## 2.1 Conformance matrix + +| # | Phase | Rust reference | Solidity | Verdict | +| --- | --- | --- | --- | --- | +| 1 | Hash function | Keccak256, plain `update`, no domain prefix (the `BLAKE2B_PREFIX_*` tags apply only to the Blake2b impl) | Streaming buffer, one `keccak256` per squeeze (`:399,:460`) | **MATCH** | +| 2 | Squeeze / reseed | `out = clone().finalize()`, then `state := Keccak::new().update(out)` (`implementors.rs:198`) | `h0 = keccak(buf)`, `mstore(TRANSCRIPT_MPTR, h0)`, cursor → +32 (`:461-467`) | **MATCH** | +| 3 | Digest → Fr | `BigUint::from_bytes_be(digest) % r` — modular reduction, not rejection sampling | `mod(h0, r)` (`:466`) | **MATCH** (identical, incl. identical bias — see note below) | +| 4 | Scalar absorb encoding | `to_repr()` (LE) reversed → 32 BE bytes | Calldata word verbatim (BE); shim pre-reverses off-chain | **MATCH** | +| 5 | Point absorb encoding | 128-byte EIP-2537 padded uncompressed; identity = 128 zero bytes (`implementors.rs:283`) | `common_uncompressed_g1` copies 4 words verbatim after pad/`Fp` checks (`:434-455`) | **MATCH** | +| 6 | Initial seeding | `vk.hash_into` → `common(&transcript_repr)`, one Fq → 32 BE bytes | `common_word(buf_len, mload(VK_DIGEST_MPTR))` (`:1091`) | **MATCH** on encoding; digest *value* not independently recomputable | +| 7 | Committed-instance commitments | Caller-supplied, absorbed and opened as real PCS queries (`verifier.rs:90,356,610`) | Hard-coded 128 zero bytes = identity (`:1101-1111`) | **MATCH only under `committed_instances = [identity]`** → **L-2** | +| 8 | Instance count + values | Absorb `F::from_u128(len)` then each value | `common_word(…, 19)` then 19 range-checked words (`:1118-1132`) | **MATCH** | +| 9 | Advice / phase order | Per phase: all advice, then that phase's challenges | One phase, 15 G1s, no user-challenge squeeze | **MATCH** | +| 10 | Challenge schedule | θ → lookup-m → β → γ → perm-Z → helpers+acc → trash (unconditional) → trashcans → y → quotient limbs → x → evals → x1,x2 → f_com → x3 → q_evals → x4 → π | Identical (`:1178,1196,1197,1256,1273,1296,1330,1331,1343,1377`) | **MATCH** — verified byte-for-byte | +| 11 | `x3` truncation | `truncate(x3)` = low 128 bits (`kzg/mod.rs:456`) | `and(mload(X3_MPTR), 2¹²⁸−1)` immediately after squeeze (`:1353`) | **MATCH** | +| 12 | `x1`/`x4` power truncation | Accumulator full precision, each emitted power truncated | `acc := mulmod(acc,x1,r)` full; stored value masked (`:3011,:3379`) | **MATCH** | +| 13 | `x2` | Not truncated | Unmasked | **MATCH** | +| 14 | Trailing-data check | `Transcript::assert_empty` | `proof_cptr == NUM_INSTANCE_CPTR` (`:1395`) + exact `calldatasize` | **MATCH** (Solidity stricter) | +| 15 | Instance evaluation | `l_i_range(x, xⁿ, …)`, inner product | Fixed `L_0…L_18` (`:1457-1466`) | **MATCH** given `max_rotation == 0` over instance queries (codegen-enforced) | +| 16 | Lagrange formula | `L_i(x) = ωⁱ(xⁿ−1)/(n(x−ωⁱ))` | `l_i_common · inv(x−ωⁱ) · ωⁱ` (`:1436-1442`) | **MATCH** | +| 17 | `l_last` / `l_blind` / `l_0` | `l_evals = l_i_range(x, xⁿ, −(b+1)..=0)`; `b = 9` | slot 0 / Σ slots 1–9 / slot 10 (`:1446-1452`) | **MATCH** | +| 18 | Identity family order | gates → permutation → lookups → trash (`plonk/mod.rs:516-607`) | Inline gate prefix → VM (gates, `NATIVE_PERMUTATION`, `NATIVE_LOOKUP`) → trash suffix | **MATCH** | +| 19 | `y`-power aggregation | Reverse fold: identity *j* gets `y^(m−1−j)`, `m = 49` | Horner from the front + selector gap/tail bookkeeping — same exponents | **MATCH** (all 21 in-bytecode gaps and 10 selector tails checked arithmetically) | +| 20 | Selector compression | Simple-selector fixed evals replaced by `ONE`, identity attributed to the gate's first simple selector | Selector eval hard-coded `0x1`; 10 buckets × `x1^42` paired with the right fixed commitments (`:3491-3510`) | **MATCH** | +| 21 | Permutation argument | `l_0(1−z_0)`; `l_last(z_L²−z_L)`; `l_0(z_i − z_{i−1}^last)`; per-chunk with `βx·DELTA^(chunk·len)` | Identical (`:2095-2130`); `DELTA = 7^(2³²)` and `DELTA³` verified numerically | **MATCH** | +| 22 | LogUp argument | `(l_0+l_last)Z`; `h·Πf − Σ_j Π_{k≠j} f_k`; `((Z_next−Z−sΣh)(t+β)+m)·active`; θ-compression | Identical, but prefix/suffix products instead of `product · f⁻¹` (`:2179-2196`) | **MATCH** on value; see L-2 note below | +| 23 | Trash argument | `fold(acc·τ + e) − (1−q)·trash_eval` | `:2861-2865` | **MATCH** | +| 24 | Quotient reconstruction | limb scalars `(1−xⁿ)(x^{n−1})^k` | `x_split = x^(2²⁰−1)`, `one_minus_x_n` (`:2942-2965, 3479-3490`) | **MATCH** | +| 25 | Expected opening scalar | `expected_eval −= eval` for the `None` group | `QUOTIENT_EVAL_MPTR := −A` (`:2921`), 43rd eval of set 0 at `x1^42` | **MATCH** | +| 26 | PCS scheme | Halo2 multipoint (GWC-style with r-poly interpolation), **not** SHPLONK | Same reduction shape | **MATCH** | +| 27 | Rotation sets | `construct_intermediate_sets` then `sort_by_key((len, i))` | 4 rotations, 5 sets of size 1,2,2,3,3 | **MATCH** for this VK | +| 28 | `x1` batching | `q_com[s] = Σ x1ⁱ C_{s,i}`, `q_eval_set[s] = Σ x1ⁱ e_{s,i}` | 43 truncated powers; per-set folds; MSM scalars `x1ⁱ·x4ˢ` | **MATCH** | +| 29 | `f_eval` | Reverse fold `acc = acc·x2 + (q_eval_s − r_s(x3))/Π(x3−p)` | Sets 4→0 (`:3214-3366`), algebraically identical form | **MATCH** | +| 30 | `v` | `inner_product([q_evals…, f_eval], truncated_powers(x4))` | `:3390-3395` | **MATCH** | +| 31 | `final_com` | `msm_inner_product([q_coms…, f_com], truncated_powers(x4))` | Single fused 78-term G1MSM (`:3396-3559`) | **MATCH** | +| 32 | Pairing / negation | `e(π, s_g2)·e(final_com − vG + x3π, n_g2) = 1` — G₂ **generator** negated | `e(final_com − vG + x3π, G₂)·e(π, −[s]G₂) = 1` — `s·G₂` negated | **MATCH** — same equation, sign carried on the other element | +| 33 | Domain constants | `EvaluationDomain` | VK words 2–6 | **MATCH — verified numerically** | +| 34 | `blinding_factors` / `rotation_last` | `max(3, max_advice_queries) + n_trash + Σchunks + 3 = 9`; `−10` | 9 negative Lagranges; `ω⁻¹⁰` | **MATCH** | +| 35 | `quotient_poly_degree` | `cs_degree − 1 = 4` | 4 quotient limbs | **MATCH** | +| 36 | G1/G2 bases | `G1Affine::generator()`, `params.g2()`, `−params.s_g2()` | VK words 11–30 | **MATCH** for G1/G2 (byte-compared to canonical generators); `NEG_S_G2` unverifiable → **H-1** | + +### Note on challenge sampling bias + +Both sides compute `keccak256(...) mod r` on a **256-bit** digest reduced into a **255-bit** prime field. This is *not* the usual "wide hash, negligible bias" situation: `floor(2²⁵⁶/r) = 2`, so 20.8% of residues have three preimages and the rest have two. Statistical distance from uniform is **7.47%**. + +That sounds bad and is not. Soundness bounds depend on the *maximum* challenge probability, which is inflated by `3/(2²⁵⁶·(1/r)) = 1.3585×` — **0.44 bits**. Negligible, and bit-identical to the Rust reference, so it is a shared property rather than a divergence. Recorded here because the naive "≈2⁻¹²⁷ bias" figure often quoted for Fiat-Shamir reductions does not apply at this digest/field width and should not be carried into a security argument. + +## 2.2 Where Solidity is *stricter* than Rust + +Worth recording, because a reader of the Rust cannot assume these: + +1. ABI head words, proof length, instance count and `calldatasize()` are all pinned (Rust takes typed arguments). +2. The VK runtime is re-pinned by `extcodesize` **and** `extcodehash` on **every** proof. +3. Six VK header words are cross-checked against baked-in constants. +4. Public instances are range-checked `< r` before absorption. +5. EIP-2537 pad canonicality is enforced — top 16 bytes zero, coordinate ≤ `p−1`. +6. `scalar_inv` rejects both `x ≥ r` and `x == 0`; Rust's `invert().unwrap()` **panics** instead. +7. `batch_invert` rejects any non-canonical or zero denominator; Rust's `ff::BatchInvert` silently **skips** zeros, yielding `L_i = 0` and continuing. +8. Quotient-VM structural post-conditions; unknown opcode reverts. +9. Every precompile call checks `returndatasize()` exactly; the pairing result is compared `== 1`, not masked. +10. Deploy-time smoke tests for MCOPY and three EIP-2537 precompiles, including a known-answer `G1ADD(G,G) = 2G` vector. +11. `ec_pairing` reverts on entry when `success` is already false. +12. Accumulator canonical-encoding checks (exact identity sentinel, unused-bit rejection, `Fp` bound, decoded-`(0,0)` rejection). +13. Every decoded accumulator point is forced through `G1MSM` even at scalar 1, purely for the precompile's validation. +14. An extra, randomised pairing equation for the public accumulator. + +## 2.3 Where Solidity is *laxer* or behaviourally different + +| Item | Difference | Assessment | +| --- | --- | --- | +| **Point validity timing** | Rust's `G1Projective::read` does on-curve + subgroup checks at read time. Solidity absorbs raw calldata into Fiat-Shamir with only pad/`Fp` checks, deferring validity to a later `G1MSM`/pairing. | Fails closed at runtime for all 34 absorbed points — I traced each to a validating precompile. But coverage is a **codegen-time** invariant, not a runtime one: an emitter change that drops a point from the MSM removes its only validation. It is also the direct cause of **M-2**. | +| **Committed instances** | Hard-coded identity; no ABI channel. | **L-2**. Sound as rendered (empirically confirmed below), unsound if the assumption is ever violated. | +| **LogUp with `f_j + β = 0`** | Solidity's prefix/suffix product computes the correct `Σ_j Π_{k≠j}(f_k+β)` and proceeds; Rust's `product * f.invert().unwrap()` **panics**. | Solidity accepts proofs on which Rust aborts. The Solidity value matches the *documented* identity, so this is a robustness gain — but it is a genuine accept/reject divergence on adversarial input and should be recorded as intentional. | +| **`x` landing on the domain** | Solidity reverts; Rust continues with `L_i = 0`. | Negligible-probability divergence. | +| **Multi-proof** | Solidity is single-proof only. | Documented non-goal. | +| **Feature coupling** | Prover and verifier must be built with matching `truncated-challenges` / `fewer-point-sets` / `single-h-commitment`. A mismatch changes the proof length and is caught by the length pin — fails closed, but with no diagnostic. | Feeds **L-8**. | + +## 2.4 Two latent codegen divergences (inert for this VK) + +- **Permutation `z_last` query order.** Rust emits all `(x, z_i)`/`(ωx, z_i)` pairs first, then the `x_last` queries in **reverse** set order. The generator emits `Cur, Next, Last` interleaved per set in forward order. For this VK the resulting point-set structure is identical, because no new rotation and no new commitment is introduced between the two placements — I traced it. It is **not** an order-insensitive transformation in general. +- **Fixed-eval counting.** Rust reads `num_fixed_columns − num_simple_selectors` (column-based); the generator counts non-simple *queries*. Both equal 17 here. A circuit with a rotated fixed query would make Rust under-read and Solidity over-read. + +Neither is a live bug. Both should become explicit generator assertions rather than accidents. + +## 2.5 What could not be verified against the reference + +1. **The accumulator path has no Rust counterpart** in `midfall/proofs` — decoding, the second pairing equation and the randomised batching are all application-level (`midnight-circuits` / Moonlight). The construction is internally sound, but the *orientation contract* (that instances 11–14 are the `[s]`-side and 15–18 the `[1]`-side, matching what the producing circuit exposes) is an assumption. Getting it backwards breaks completeness, not soundness. +2. **`vk_digest = 0x56c0824f…4f66`** cannot be recomputed without the concrete `ConstraintSystem`. +3. **`NEG_S_G2_BASE`** cannot be checked without the ceremony transcript → **H-1**. +4. **The quotient VM bytecode's arithmetic content.** All *structural* properties were verified — 49 identities in the right order, correct `y` exponents, every operand pointer landing inside the absorbed-evaluation window `[0x9480, 0xb140)`, constant-table indices within the 178-word reservation, clean termination. What was not verified is that the ~29 emitted gate expressions encode the intended circuit; that needs the circuit definition. +5. **Upstream `construct_intermediate_sets`** (`src/poly/kzg/utils.rs`) was not available; the generator's re-implementation could only be checked for internal consistency. Note the generator's own caveat: it groups commitments **by memory pointer** where Rust groups **by value**, which is only safe under the single-committed-instance restriction. + +--- + +# Part III — Findings + +Severity reflects impact on the *deployed system*, not on the Solidity in isolation. Line references without a filename are `Halo2Verifier.sol`. + +--- + +## H-1 — High — `NEG_S_G2_BASE` is unverifiable trusted-setup material with no ceremony binding and no build-time consistency check + +**Where:** VK contract lines 88–95 (`neg_s_g2_*`); generator `src/lowering/vk.rs:93-98`. + +```rust +let g1_pt: G1Affine = G1Affine::generator(); +let g2_pt: G2Affine = self.params.g2().to_affine(); +let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); +``` + +`neg_s_g2` is taken verbatim from whatever `params` the generator was handed. The G1 side **is** defended — `vk.rs:81-92` asserts `sum(g_lagrange) == G1Affine::generator()` with a good comment explaining why. There is no analogous check on the G2 side: + +- nothing asserts `params.g2() == G2Affine::generator()`; +- nothing checks that `s_g2` corresponds to the *same* τ that produced `g_lagrange`; +- nothing binds either point to a named ceremony transcript; +- no SRS file hash, ceremony id or transcript URL is recorded anywhere. `CODEGEN_ASSURANCE_DOSSIER.md:46` lists "SRS assumption | Midnight SRS asset names, sizes, and expected source" as a **required manifest record**; no such record exists. The only provenance is a URL in `README.md:192` and `SRS_DIR=…/zk_stdlib/examples/assets` in the fixture README. + +**What I could verify:** `G1_BASE` and `G2_BASE` are exactly the canonical BLS12-381 generators. `NEG_S_G2_BASE` is a valid on-curve point of E′(Fp2) with `b = 4(1+i)`, in the r-order subgroup, and is not ±G₂. That is the limit of what is checkable without the transcript. + +**Failure scenario.** An attacker substitutes the SRS file in `SRS_DIR` with one whose τ they know — or a build host is compromised. The generator emits `neg_s_g2 = −[τ_evil]G₂`. Every downstream control passes: `certify_quotient_program`, `certify_quotient_builds_agree`, `validate_generator_invariants`, the codehash pin, the trace differential, the replay fixture. All of them check *self-consistency*, and the artefact is perfectly self-consistent. The attacker then forges KZG openings for arbitrary statements and `verifyProof` returns `true`. Nothing on-chain or at build time detects it. + +**Remediation** (all implementable with the API already in use): + +1. `assert_eq!(self.params.g2().to_affine(), G2Affine::generator())`. +2. Add a build-time SRS pairing consistency check: commit the polynomial `f(X) = X` in the Lagrange basis (`Σ ωⁱ·L_i`, from the `g_lagrange` the generator already consumes) to obtain `[τ]G1`, then assert `e([τ]G1, G2) == e(G1, s_g2)`. This proves `neg_s_g2` is the negation of the same τ that generated the commitment basis. +3. Pin the SRS by SHA-256 in the generator; record it and the ceremony transcript reference in the artefact manifest; fail the render on mismatch. +4. Publish `neg_s_g2` alongside the ceremony's published τ point so a third party can verify the negation independently. + +Patch **P11** implements (1) and (2). + +--- + +## M-1 — Medium — `assembly ("memory-safe")` is factually false, and the invariant that protects it is enforced only outside the artefact, under a floating pragma + +**Where:** lines 203, 336, 345 (the annotations); lines 58–59 (`TRANSCRIPT_MPTR = RETURN_MPTR = 0x1000`); first write at line 463. Template: `templates/contracts/Halo2Verifier.sol:2,98`. + +All three assembly blocks are annotated `("memory-safe")` but write to absolute addresses (`0x1000 … 0xe340`) never derived from the free-memory pointer. The generator's own documentation admits it (`docs/architecture/MEMORY_LAYOUT.md`, "Solidity Memory Model Boundary"): + +> "…also factually untrue … The annotation is what enables solc's stack-to-memory mover, which reserves spill slots upward from `0x80` … Observed reservations range from `0x80` (none) to `0x8e0`." + +The mitigation lives entirely **outside** the shipped artefact: a generator-side test, `compiled_memoryguard_does_not_overlap_generated_layout`. The `.sol` has no runtime assertion, and the pragma is floating (`^0.8.24`), so a downstream integrator recompiling with a different solc release, `--via-ir` setting or optimiser schedule can move the reservation with no signal. + +**Measured headroom.** I compiled the artefact across four compiler versions and extracted the runtime prologue's `mstore(0x40, X)`: + +| solc | viaIR | runs | runtime bytes | FMP init | headroom to `0x1000` | +| --- | --- | --- | --- | --- | --- | +| 0.8.24 | true | 1 | 29,567 | `0x8c0` | 1,856 B | +| 0.8.26 | true | 1 | 21,295 | `0x8e0` | 1,824 B | +| 0.8.28 | true | 1 | 21,286 | `0x8e0` | 1,824 B | +| 0.8.30 | true | 1 | **21,286** | `0x8e0` | 1,824 B | +| 0.8.30 | true | 200 | 21,318 | `0x8e0` | 1,824 B | +| 0.8.30 | true | 100000 | 29,836 | `0x8e0` | 1,824 B | + +So the reservation **does vary with compiler version** (`0x8c0` vs `0x8e0`) — the invariant is not a fixed quantity — and the surviving margin is 57 words, unmonitored at runtime. + +**Failure scenario.** Compile with a solc/optimiser combination whose stack-limit evader reserves ≥ `0xF80` bytes. The prologue then emits `mstore(0x40, X)` with `X > 0x1000` and a live Yul spill slot sits at ≥ `0x1000`. The first transcript write — `mstore(TRANSCRIPT_MPTR, h0)` at line 463, or `calldatacopy(buf_len, cptr, 0x80)` at line 453 — overwrites it. If the clobbered local is `success` (line 974) or `proof_cptr` (line 1155), the terminal checks at 1395 / 3666 / 3678 read attacker-influenced garbage. + +**A second, immediately concrete consequence.** The floating pragma is not merely theoretical. I compiled and attempted deployment under `revm` Prague: + +``` +solc 0.8.24 runs=1 : DEPLOY FAILED -- HALT CreateContractSizeLimit (29,567 B > 24,576) +solc 0.8.30 runs=1e5 : DEPLOY FAILED -- HALT CreateContractSizeLimit (29,836 B > 24,576) +solc 0.8.30 runs=1 : DEPLOYED +``` + +**The contract as shipped is undeployable at the minimum compiler version its own pragma permits.** `^0.8.24` advertises compatibility the artefact does not have. + +**Remediation.** Two lines, both cheap: + +```solidity +pragma solidity 0.8.30; // pin exactly +``` +```yul +if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } // ~6 gas, once +``` + +The guard makes the invariant self-enforcing in the deployed bytecode instead of depending on a test in another repository. Patches **P2** and **P3**. + +*Related:* the constructor's `require_eip2537_precompiles()` runs in the **creation** frame under the same false annotation and writes `0x1000`–`0xe200`, but the generator's guard test inspects only the *runtime* prologue (`src/evm.rs:233-268` is explicitly typed for runtime). Fail-closed in practice (a corrupted `authorizedVk` spill would fail the codehash `require`), but the asymmetry looks unintended. Also: the shipped comments at lines 316–317 and 347 claim "generated scratch starts at `0x80`", contradicting `TRANSCRIPT_MPTR = 0x1000` on line 58 — this is precisely the stale belief the team's own TA-5 finding identified as the hazard, and it is hardcoded in the template so **every** render ships it (**I-1**). + +--- + +## M-2 — Medium — A malformed curve point burns ≈63/64 of the supplied gas instead of reverting cheaply + +**Where:** every `staticcall(gas(), 0x0c, …)` — lines 911, 956, 3558, 3573, 3584, 3629, 3643 — and the deferred-validation policy documented at lines 425–429. + +Because `common_uncompressed_g1` deliberately does **not** run a curve check (§2.3), an invalid point survives until it reaches a `G1MSM`. EIP-2537 precompiles that reject their input **consume all gas forwarded to them**, and `staticcall(gas(), …)` forwards 63/64 of what remains. + +**Measured** (revm Prague, single flipped byte in `advice[0].x_lo`): + +| tx gas limit | off-curve point | invalid evaluation | bad EIP-2537 pad | valid proof | +| --- | --- | --- | --- | --- | +| 2,000,000 | **1,979,170** | 1,279,485 | 315,950 | — | +| 5,000,000 | **4,932,295** | 1,279,485 | 315,950 | — | +| 30,000,000 | **29,541,670** | 1,279,485 | 315,950 | — | +| 500,000,000 | **492,197,920** | 1,279,485 | 315,950 | 1,279,482 | + +A cryptographically-invalid proof costs a bounded 1.28M. A structurally-malformed one costs 316k. But a proof containing **one byte** that puts a point off-curve costs whatever you were willing to spend — 98.5% of it, at zero marginal cost to the person who chose that byte. + +**Failure scenario.** Any component that pays gas on someone else's behalf or continues after a failed verification: + +- A relayer / paymaster / ERC-4337 bundler submitting user proofs pays 30M instead of 1.3M — a 23× amplification, chosen by the user, per transaction. +- A wrapper doing `try verifier.verifyProof(…) { } catch { }` regains control with 1/64 of the gas, almost certainly too little to finish, so the whole transaction reverts anyway — the `catch` branch becomes unreachable in practice. +- A batch verifier looping over N proofs is destroyed by one bad point in the first. + +**Remediation.** Cap the gas forwarded to the validating calls. EIP-2537 `G1MSM` pricing is a pure function of input length (`k · 12000 · discount[k] / 1000`), with no data dependence, so an **exact** constant is computable at codegen time; a 2× margin absorbs any schedule change: + +```yul +// was: staticcall(gas(), 0x0c, ptr, len, out, 0x80) +staticcall(G1MSM_GAS_78, 0x0c, ptr, len, out, 0x80) +``` + +This turns a 30M burn into a bounded ~1M. Patch **P1**. Note the cap must be generous and codegen-derived — a hand-tuned value that is too tight becomes a liveness bug on a chain with a different schedule. + +--- + +## M-3 — Medium — The deployment probe is weakest exactly where acceptance is decided + +**Where:** lines 202–286 (`require_eip2537_precompiles`); template `PrecompileSmoke.sol`. + +The smoke test hardens `G1ADD` with a known-answer vector and states the reasoning explicitly and correctly (lines 229–238): + +> "Every probe above uses the point at infinity, which is exactly the input an implementation gets right without doing any curve arithmetic — a precompile that returns its zero-filled input, or zeros for anything, satisfies them… So add one vector whose answer a stub cannot guess." + +That reasoning was **not applied to the two precompiles that decide the outcome**: + +- **`0x0f` PAIRING_CHECK** (line 282) is probed only as `PAIRING_CHECK([(0,0),(0,0)]) == 1`. A chain whose `0x0f` returns `1` unconditionally, or omits G2 subgroup checks, passes deployment and then accepts **every** proof — `ec_pairing` is the sole accept gate. +- **`0x0c` G1MSM** (line 272) is probed with `0x30c0` bytes of all-zero terms. Line 270 says "the production verifier uses G1MSM… **as the subgroup validator** for absorbed proof points" — and then tests it with 78 identity terms, exercising no rejection behaviour at all. + +The team already found this. `AUDIT.md` **TA-6, "Precompile Smoke Tests Are Too Weak For Deployment Confidence"**, is rated **Low**, carries **no `Status:` line** (it is un-triaged — every other TA item has one), and recommends exactly: known-valid and known-invalid pairing equations, invalid G1/G2 encodings that must fail, and boundary scalars `0, 1, r−1, r`. None are present. `ARCHITECTURE_REVIEW_2026-08.md` §7 concedes the residual state: *"EIP-2537 semantics (incl. subgroup checks) | Assumed per spec; constructor smoke proves existence/arithmetic, not rejection behavior | **Assumed**"*. + +**Verified good:** the strict `returndatasize()` checks do make the verifier fail closed on a chain **without** EIP-2537 — a staticcall to an empty account succeeds with `returndatasize() == 0`. I confirmed this: the constructor reverts under `SpecId::CANCUN`, `SHANGHAI` and `MERGE`. So *existence* is genuinely proven; *correctness* is not. + +**Remediation.** Add constant-size known-answer probes (deployment-gas cost only): a true pairing `e(G,G₂)·e(−G,G₂) == 1`, a **false** pairing `e(G,G₂)·e(G,G₂) == 0`, a `G1MSM` known answer `[2]·G == 2G`, and a negative probe with a known wrong-subgroup encoding that must cause the precompile to fail. Give TA-6 a `Status:` and re-rate it — with the whole accept path resting on `0x0f`, Low is too low. Patch **P5**. + +--- + +## M-4 — Medium — `vk_digest` does not cover the SRS points, the quotient program, the accumulator schema, or the feature profile + +**Where:** `../proofs/src/plonk/mod.rs:246-277`; line 1091 (absorption); lines 1018–1023 (header cross-check). Team's own finding: `AUDIT.md` TA-2, and item 2 of their "Time-Boxed Audit Priorities Before Production". + +`transcript_repr` hashes `VERSION`, `k`, fixed commitments, permutation commitments, and the `Debug` strings of `domain.pinned()` / `cs.pinned()`. It does **not** cover `G1_BASE` / `G2_BASE` / `NEG_S_G2_BASE`, the quotient VM constant pool or bytecode, the accumulator schema, or the Cargo feature profile (`truncated-challenges`, `outer-fewer-point-sets`, `outer-single-h-commitment`). The transcript therefore does not bind them. + +The on-chain header cross-check covers 6 of 31 header words (`num_instances`, `k`, `has_accumulator`, `acc_offset`, `num_acc_limbs`, `num_acc_limb_bits`). `vk_digest`, `n_inv`, `omega`, `omega_inv`, `omega_inv_to_l` and all three base points are not cross-checked. Codehash pinning makes this non-exploitable post-deploy, but the comment's claim that these checks "catch generator drift" is narrower than it reads. + +**On the wrong-VK question specifically:** a wrong-but-well-formed VK **cannot** be paired with this verifier — the constructor reverts on codehash mismatch, and the loader re-checks on every proof. I confirmed both empirically (a 1-bit-mutated VK and an EOA address both revert the constructor). But a VK generated from a *different SRS or feature profile* for the same circuit produces a self-consistent verifier+VK pair that no digest distinguishes. That residual compounds H-1 and L-8. + +**Remediation.** Absorb `EXPECTED_VK_CODEHASH` into the transcript, or redefine `vk_digest` over the full runtime payload — the team's own TA-2 recommendation, still open. + +--- + +## M-5 — Medium — The build is not reproducible as documented + +**Where:** `fixtures/moonlight-wrap/README.md:17`, `docs/audit/REVIEW_PACKET.md:60-84`, `docs/audit/CODEGEN_ASSURANCE_DOSSIER.md:34-47`, `src/evm.rs:179-183,208`, `tests/ivc_accumulator_replay.rs:55`, `scripts/install_pinned_solc.sh:29-34`. + +Three mutually unreconcilable provenance stamps exist for this artefact: + +| Source | Stamp | +| --- | --- | +| `fixtures/moonlight-wrap/README.md:17` | solidity-verifier commit `3fb6d84` | +| `REVIEW_PACKET.md:67` | repository commit `a096e71746e401404f250817ca4e857bac1eef56` | +| `CODEGEN_ASSURANCE_DOSSIER.md:36` | Midfall revision `53dc872f495104046d96bdac0a690f903dc0c537` | + +> Correction (2026-08-12): the three stamps are not actually contradictory — +> they index different things (fixture-render commit of this repo, packet-time +> commit of this repo, and the Midfall *dependency* revision). They were +> merely unlabeled. Each source now says which identity it records; the +> canonical table is "Provenance Identities" in +> `docs/reference/REPRODUCIBLE_BUILDS.md`. (This row's dossier citation was +> also off by one — `:37` → `:36`.) + +`REVIEW_PACKET.md` §4 "Artifact Manifest" — the table meant to pin *this* artefact — reads `fill per artifact` for all eleven hash rows. `docs/reference/REPRODUCIBLE_BUILDS.md`, cited from three places as where the hashes live, was not in scope. + +The recorded solc flag list is also incomplete. Both the dossier (`:40`) and the packet (`:71`) record `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` — **`--optimize-runs` is missing**. `src/evm.rs:179-183` reads it from the environment (`SOLC_OPTIMIZE_RUNS`, default `200`), while the one test that exercises this fixture pins a *different* value (`tests/ivc_accumulator_replay.rs:55: const SOLC_OPTIMIZE_RUNS: u32 = 1`). + +**This is not cosmetic.** As measured under M-1, the setting is load-bearing for *deployability*: at `runs=100000` the runtime is 29,836 bytes and the deployment halts with `CreateContractSizeLimit`. A "reproducible" build whose success depends on an unrecorded environment variable is not reproducible. + +Separately, `install_pinned_solc.sh` downloads the compiler over HTTPS and validates it only by its own `--version` output, which a substituted binary forges trivially. `binaries.soliditylang.org` publishes SHA-256 and keccak-256 for every release in `list.json`; the script uses neither. For a project whose entire reproducibility claim rests on the string `0.8.30+commit.73712a01`, pinning by version string rather than content hash is the wrong pin. (`Darwin-arm64` also maps to `macosx-amd64`, so Apple Silicon developers run an x86 compiler under Rosetta — a different binary from CI, unchecked.) + +**Remediation.** Fill the manifest for the real deployment artefact; add `--optimize-runs ` to the recorded flags and remove the env override from the reproducible path; reconcile the three stamps to one; ship `REPRODUCIBLE_BUILDS.md` with the artefact; verify solc by SHA-256. Patch **P13**. + +**Credit where due:** the VK half *is* solidly reproducible. Its runtime is pure returned data, so `keccak256(0xfe ‖ payload) = 0xe68d8936…cf52` at length 17,025 is solc- and optimiser-independent. I recomputed it from source and it matches. That is a good design property worth preserving. + +--- + +## L-1 — Low — Codegen certification covers the emitter leg only; the leg that executes on-chain is fixture-sampled + +**Where:** `src/lowering/quotient_numerator/vm/certify.rs:129-225`, `vm/reference.rs:20-27`, `src/lowering/plan.rs:167-183`, `tests/ivc_accumulator_replay.rs:18-45`. + +The certification is real and unconditional — `LoweringPlan::new` panics the render if `certify_quotient_program` or `certify_quotient_builds_agree` fails, the challenge seed derives from the finalised bytecode + constant table + expression trees + VK payload, and the dual build with recognizers disabled turns every shape recognizer into a checked optimisation. That is better than most generated-verifier projects. Its coverage boundary is narrow, though, and the boundary is where a supply-chain attack would land: + +1. **The front end is uncovered.** Certification compares emitted bytecode against the `QuotientExpr` the lowering itself produced. A bug in the halo2-`Expression` → `QuotientExpr` translation is wrong on both sides and agrees with itself. The team identified this exact hazard for *pointers* and built `validate_quotient_mem_ptrs` (`plan.rs:346-348`: *"certification cannot do this: it compares the bytecode against the expression tree, so a pointer that is wrong in both agrees with itself"*) — but not for expressions. +2. **Native kernels are uncovered.** `NATIVE_PERMUTATION` / `NATIVE_LOOKUP` / `NATIVE_IDENTITY` are checked only for stream position (`certify.rs:180`: *"Native markers carry no arithmetic here."*). The permutation and LogUp kernels are trusted code. +3. **The on-chain Yul VM is uncovered.** `reference.rs:20-27` scopes it explicitly: the reference-interpreter → Yul leg is covered "by the opcode and memory-token table conformance tests and by the per-identity Rust/Solidity trace differential on fixture circuits". §7 adds: *"an opcode no fixture emits has unverified runtime semantics."* + +**Direct answer to "could a malicious edit produce an accepting verifier that still passes the repo's own tests": yes.** Edit `templates/partials/quotient_numerator/*.yul` or a native kernel. `certify` never touches Yul. The SRS-free replay test compiles **committed pre-rendered `.sol` from the fixture directory**, not fresh template output — its own header says so: *"the replay keeps passing after a codegen change — it just stops testing current output."* The only gate that would catch it is the SRS-gated trace differential, which per §10 runs weekly / on push-to-main, not in the default CI job. + +**Remediation.** Pin a digest of the `templates/` tree as a constant checked by a CI test, so any template edit forces an explicit fixture re-render and README commit-stamp bump. Needs no SRS. Longer term: extend certification to the front end by evaluating `vk.cs()` expressions directly at the same challenge assignment, and add a native-kernel differential independent of fixture coverage. Patch **P14**. + +--- + +## L-2 — Low — Committed-instance commitment hard-wired to the identity; one MSM term silently omitted while the emitted comment claims otherwise + +**Where:** lines 1101–1111 (128 zero bytes absorbed); line 3023 (`// q_eval_set[0]: 43 commitment(s)`); lines 3397–3399 (the MSM skips `x1^1`). Generator: `src/lowering/encoding/mod.rs:255-258, 409-415`. + +The eval side computes `q_eval_set_0 = Σ_{i=0..42} x1ⁱ · eval[table[i]]` — 43 terms. The commitment side materialises only 42: line 3397 stores scalar `1`, then line 3399 jumps straight to `x1^2`. The orphaned eval is `table[1] = REVERSED_EVALS[0] = 0x9480`, the committed-instance column's evaluation. The omission is consistent with `committed_pi = G1Affine::identity()` (an identity commitment contributes nothing to an MSM), but **no comment, constant or runtime check states it**, and the emitted comment on line 3023 actively contradicts the emitted code. + +**Empirically confirmed sound as rendered.** In the fixture, `eval[0] = 0x0000…0000`. Setting it to any other value is rejected (test N4, reject at 1,279,485 gas — i.e. it runs to the pairing and fails there). This is exactly the expected behaviour: with an identity commitment the batch check pins the claimed opening to zero, and `x1^1 ≠ 0`, so the column is fully constrained. Omitting the identity term from the MSM is a gas optimisation, not a soundness gap. + +**The risk is drift, not the current render.** If a future render ever needs a non-identity committed-instance commitment, this verifier would keep hashing 128 zero bytes and keep omitting the term, verifying the wrong statement with no runtime signal. The counts (43 vs 42), the exponent multiset, and the `0x30c0` MSM length are all independently hardcoded, so the discrepancy is invisible to every existing check. + +**Remediation.** Either emit the identity term explicitly (`G1_IDENTITY_MPTR` at line 144 exists for exactly this and is currently unused — dead), or add a generator invariant asserting that per point set the multiset of `x1` exponents in the MSM equals `{0 … m−1}` minus exactly the indices of commitments *proven* to be the identity, and render a comment naming the omitted index. Patch **P7**. + +--- + +## L-3 — Low — 41 bare `revert(0, 0)` sites; `verifyProof` can never return `false` + +**Where:** 41 sites including 212, 338, 367, 439, 610, 1007, 1024, 1048, 1064, 1133, 1316, 1369, 1395, 1482, 2620, 3666, 3678. The only reason-carrying failure is the constructor's `require(…, "invalid vk")`. + +At the call site, "malformed calldata shape" (338), "VK code changed under us" (1007), "non-canonical instance" (1133), "bad proof" (3666) and out-of-gas are all indistinguishable — empty returndata. That materially degrades incident response: you cannot tell a VK swap from a bad proof. + +Separately, line 3679 unconditionally stores `1`, so `verifyProof` returns `true` or reverts; it never returns `false` despite `returns (bool)`. This is documented at line 324, but integrators writing the idiomatic `if (!verifier.verifyProof(p, i)) { … }` get a bubbled empty revert rather than the false branch. + +**Remediation.** Give each failure class a 4-byte custom-error selector — `mstore(0x00, shl(224, sel)); revert(0x00, 0x04)` — writing at `0x00`/`0x04` is legitimate scratch and does not disturb the layout. At minimum distinguish ABI-shape, VK-mismatch, canonicality, precompile-failure and pairing-failure. Patch **P4**. + +--- + +## L-4 — Low — The exact-`calldatasize` pin makes the verifier uncallable through calldata-appending relayers + +**Where:** lines 1042–1045 (`calldatasize() == 0x2144`). + +The check is correct and fail-closed, but it means the verifier cannot be called through anything that appends calldata: ERC-2771 trusted forwarders append the 20-byte original sender; several relayer, multicall and paymaster patterns append context words. Confirmed empirically — test N25 (20 appended bytes) reverts, as does N24 (32 appended zero bytes). + +The four other pins (head words at 337, proof length at 1037, instance count at 1038, terminal `proof_cptr == NUM_INSTANCE_CPTR` at 1395) already make trailing bytes unreadable by the parser, so relaxing to `>=` loses nothing. Either relax it, or state the constraint in the `verifyProof` NatSpec — right now an integrator hits an empty revert with no diagnostic (see L-3). + +--- + +## L-5 — Low — `CHALLENGE_MPTR` aliases `THETA_MPTR` + +**Where:** line 83 (`CHALLENGE_MPTR = 0x7900`), line 89 (`THETA_MPTR = 0x7900`), line 1178. Template `TranscriptProofParser.yul:127`, `Constants.sol:66`. + +Inert here: this circuit has zero user-phase challenges, so the template's per-phase squeeze loop rendered nothing, and `CHALLENGE_MPTR` is referenced only by its own declaration. + +**For any circuit with ≥ 1 user-phase challenge**, the template emits `squeeze_to(buf_len, add(CHALLENGE_MPTR, 0x0))` → writes `0x7900`; line 1178 then unconditionally does `squeeze_to(buf_len, THETA_MPTR)` → overwrites `0x7900`. Every later read of the user challenge silently returns theta. The transcript still matches the prover (the squeezes happen in order), so the proof simply fails to verify — a permanent, silent liveness break; and if a gate happens to be satisfied under the substitution, a soundness break. + +**Remediation.** Give `CHALLENGE_MPTR` its own window and add a planner assertion that the challenge window and the theta-window slots do not intersect. Patch **P8**. + +--- + +## L-6 — Low — Quotient-VM operands are unvalidated raw memory pointers + +**Where:** `q_ptr := shr(240, mload(q_pc))` at 1798, 1832, 1839, 1885, 1931; `and(shr(232, q_word), 0xffff)` at 1919, 1937, 1981; `q_sel_idx` / `q_sel_gap` at 2628–2648; stack arithmetic at 1801, 1811, 1910. + +Every operand is a `u8`/`u16` used directly as a memory address (`mload(q_ptr)`, `mload(add(q_const_mptr, shl(5, qconst)))`, `mstore(add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)))`) with no bound check. `q_sel_gap` is 16 bits but the y-power table holds only 49 entries, so `gap > 48` reads into the VM stack region. `case 0x06` does `q_sp := sub(q_sp, 0x20)` with no underflow guard, and the terminal `eq(q_sp, 0xb8e0)` check does **not** catch a *balanced* underflow. + +None of this is attacker-reachable today: the program lives in the codehash-pinned VK payload, and I confirmed no code path writes into `0x3680…0x7900` after the `extcodecopy`. The safety argument is entirely "the emitter never produces such a program" — there is no on-chain validator. **This becomes Critical if the VK is ever made upgradeable or parameterised.** + +**Remediation.** Clamp operands at decode time (~10 gas each): `if or(lt(q_ptr, 0x9480), gt(q_ptr, 0xb140)) { revert(0,0) }`; `q_sel_gap < 49`; `q_sel_idx < 10`; `if lt(q_sp, 0xb900) { revert(0,0) }` before the `sub`. Patch **P12**. + +--- + +## L-7 — Low — Zero-slack memory adjacencies; `batch_invert` scratch would corrupt silently + +**Where:** lines 486–599, 1431; `BATCH_INV_SCRATCH_MPTR = 0xb140`, `LAGRANGE_DENOMS_MPTR = 0xb580`. + +I recomputed all of these; every one is exactly correct and every one has **zero** slack: + +- With the 30 Lagrange inputs, `batch_invert`'s forward pass writes 28 prefix products to `0xb140…0xb4c0` and the modexp frame to `0xb4c0…0xb580` — ending *exactly* at `LAGRANGE_DENOMS_MPTR`. One more denominator and the frame's first word (the literal `0x20` base-length) overwrites `denominator[0]`; the backward pass then reads it and produces silently wrong inverses **with no revert** — the modexp still succeeds and `ret` stays 1. +- VK payload `0x3680 + 0x4280 = 0x7900` = `CHALLENGE_MPTR` exactly. +- `REVERSED_EVALS 0x9480 + 0xcc0 = 0xa140` = `ADVICE_COMMS_MPTR_BASE` exactly. +- The proof-commitment regions chain exactly to `0xb140`. +- Fused MSM `0xb280 + 0x30c0 = 0xe340` exactly. + +The `batch_invert` case is the dangerous one because it fails *silently*. Add an explicit planner assertion `scratch_len >= (n−2)·32 + 0xc0`. Patch **P9**. + +*(I also hand-verified `batch_invert`'s algorithm for n = 2, 3, 4 and 30 — the prefix/backward-pass indexing is correct, and the one-past-the-end `gp_mptr` decrement in the final iteration is computed but never dereferenced.)* + +--- + +## L-8 — Low — No on-chain provenance + +**Where:** artefact-wide; `src/evm.rs:208` (`--no-cbor-metadata`); `ARCHITECTURE_REVIEW_2026-08.md` §8: *"Feature flags ↔ expected proof schema | **Recorded nowhere in the artifact**"*. + +Nothing in the deployed runtime identifies the generator commit, the feature profile, the circuit, or the SRS. CBOR metadata is stripped, so Sourcify/Etherscan metadata matching is unavailable and source verification requires exact-flag recompilation — which M-5 shows is not fully specified. + +Proof-schema mismatches fail closed (rejected proofs, not accepted ones), so this is an incident-response problem rather than a soundness one. But during an incident, "which of our deployed verifiers has the affected codegen?" is unanswerable from chain state. + +**Remediation.** Emit `bytes32 public immutable BUILD_ID = H(generator_commit ‖ feature_list ‖ vk_digest ‖ EXPECTED_VK_CODEHASH ‖ srs_hash)`. One immutable, zero runtime cost on the verify path, and fleet inventory becomes mechanical. Patch **P10**. + +--- + +## L-9 — Low — No incident-response story, and no domain binding + +**Where:** artefact (zero `sstore` / `delegatecall` / `selfdestruct` / owner / pause / upgrade); line 1091; `AUDIT.md` TA-8, F-4. + +Immutability is the right call here: no admin key means no admin-key compromise, and `verifyProof` stays `external view`. Deploy ordering is enforced fail-closed (VK first; a wrong VK reverts the constructor — confirmed empirically). Two consequences are undocumented: + +**(a) Incident response.** If a soundness bug is found post-deployment there is no on-chain mitigation whatsoever. The only lever is the application wrapper. Nothing in the repo states the corollary requirement: **every wrapper must hold the verifier behind a replaceable address and must have its own pause.** A wrapper that hardcodes the verifier as `immutable` or `constant` has no recovery path at all. This belongs in the generated NatSpec. + +**(b) Cross-chain / cross-deployment replay.** The transcript begins at `vk_digest`. There is no `chainid`, no verifier address, no domain separator anywhere. A proof valid against this bytecode is valid against **every** deployment of this bytecode on **every** chain, forever. The NatSpec does tell wrappers to bind chain/domain — the right architectural split for a raw verifier — but a deployer who treats `verifyProof == true` as authorisation has an unconditional cross-chain replay. + +--- + +--- + +## Informational + +**I-1 — Comments contradict constants.** Lines 316–317 and 347 claim "generated scratch starts at `0x80`"; line 58 says `TRANSCRIPT_MPTR = 0x1000`. Hardcoded in `templates/contracts/Halo2Verifier.sol:54-59,98-99`, so every render ships it. Both also cite `docs/MEMORY_LAYOUT.md`; the real path is `docs/architecture/MEMORY_LAYOUT.md`. This is exactly the stale belief TA-5 identified as the hazard. Patch **P6**. + +**I-2 — Dead code and contradictory documentation.** `QUOTIENT_RETURN_MPTR` (154), `Q_COM_MPTR` (132), `G1_IDENTITY_MPTR` (144, whose comment describes "the four `mload`s below" that do not exist), `TRACE_U256_MPTR` (160) are all declared and never used. `X_N_MINUS_1_INV_MPTR` is written at 1475 and never read. The `r` parameter of `validate_public_accumulator` (869) is never used in the body. The VM opcode table (1765–1776) documents ~22 opcodes; 11 are implemented — `FOLD_MAIN (0x0a)` and `MUL (0x07)` are described in prose but have no `case`. The constructor's MSM smoke window (`0xb140…0xe200`) differs from the production window (`0xb280…0xe340`); same length, different base, so it does not pre-expand the actual range despite the comment's claim. + +**I-3 — Unreachable defensive branch.** `load_acc_point` lines 830–839 handle "x carried the identity flag but the whole-point sentinel did not match". Given the packing check makes the codec a bijection, `x_is_id` implies x's words are exactly the canonical identity words, and requiring `y` to decode to zero implies y's words are too — so `is_acc_encoded_identity` would already have returned true. Harmless defensive dead code; worth a comment saying so rather than leaving a reader to derive it. + +**I-4 — The audit chain's evidence map points at a source tree that no longer exists.** `CODEGEN_ASSURANCE_DOSSIER.md:57-65` maps every checkpoint to `src/codegen/protocol.rs`, `src/codegen/evaluator.rs`, `src/codegen/pcs.rs`, `src/transcript.rs` and `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul`. **None of the Rust paths exist.** The tree is `src/lowering/*`. (Correction +2026-08-12: this finding itself overreached on one item — +`templates/partials/quotient_numerator/QuotientNumeratorBlock.yul` DOES exist, +alongside `QuotientHelpers.yul`; the dossier row citing it was correct.) `AUDIT_FINDINGS.md`'s "Files reviewed (deep read)" list is entirely `src/codegen/*`; finding M1 anchors at `src/codegen/util.rs:443-448`. `ARCHITECTURE_REVIEW_2026-08.md` §11 concedes the drift and defers to a companion assessment that was not in scope. Consequence: the ~20 findings marked "Fixed with named tests" in the 2026-05-11 addendum could not be re-verified against the current tree. + +**I-5 — No transcript domain separation.** The Keccak transcript is a bare `keccak256` over concatenated raw bytes, matching Rust. This is weaker than the Blake2b transcript in the same file, which uses a personalisation string plus `COMMON`/`CHALLENGE` byte tags. Not exploitable here — every absorb is fixed-length and every count is pinned and re-checked, so no two distinct valid inputs produce the same byte stream, and `vk_digest` provides cross-circuit separation. A missing defence-in-depth layer, not a vulnerability. + +**I-6 — Two latent codegen divergences.** See §2.4 — permutation `z_last` query order, and column-vs-query fixed-eval counting. Inert for this VK; should be explicit assertions. + +**I-7 — The accumulator batching randomiser is not bound to `vk_digest`.** `alpha = keccak256("pairing-batch-acc-kzg" ‖ 4 G1 points)` (line 3612). Sound as-is — the four points transitively commit to everything, and `alpha` is drawn after they are final. Including `vk_digest` in the preimage is free and would make the binding local rather than transitive. + +--- + +## Prior-findings hygiene + +| ID | Claim | Verdict against this artefact | +| --- | --- | --- | +| 2026-05-06 #1 | VK codehash only checked in constructor | **Closed.** Re-checked per proof, lines 1004–1007. Confirmed empirically. | +| F-4.1 | Pathological `num_limb_bits` from VK | **Closed.** Header cross-check hardcodes 56/7. | +| F-4.2 | Dead `if and(eq(coord,1), 0)` branches | **Closed.** Absent. | +| F-6 | `delta` literal only checked off-line | **Value verified** (`0x8634d0aa…189d7` is the correct `Fr::DELTA`), but still a bare literal — the "nothing in the build forces a check" complaint stands. | +| TA-4 / F-3 | Gas checkpoints in production | **Closed.** Zero LOG opcodes; `external view`; single terminal return. | +| TA-5 | False `memory-safe` annotation | **Consequence removed** (layout at `0x1000`); annotation still false; guard is runtime-only; shipped comments still say `0x80`. → **M-1**, **I-1**. | +| TA-5 rec. | "Use `pragma solidity 0.8.24;` or the exact version tested" | **Not done.** Both artefacts still carry `^0.8.24` while the pinned compiler is 0.8.30 — and 0.8.24 does not produce a deployable contract. → **M-1**. | +| TA-2 / priority #2 | Define and test `vk_digest` coverage | **Open.** → **M-4**. | +| TA-6 | Precompile smoke tests too weak | **Partially addressed** (G1ADD known-answer only); no `Status:` line; pairing/G1MSM recommendations unimplemented. → **M-3**. | +| TA-7 | Zero-denominator / root-of-unity cases | **No status recorded.** The code does fail closed (verified), but the recommended forced-challenge negative tests were not found. | +| TA-8 / F-4 | Raw verifier binds no application semantics | **Documented** in NatSpec, correctly. Residual: no incident-response/migration guidance. → **L-9**. | +| 2026-05-11 #9 | Identity committed-instance policy | **Accepted restriction**, correctly scoped. → **L-2**. | +| 2026-05-11 #1–#8, #10; M1, M2 | Various, marked Fixed | **Could not re-verify** — anchors point at `src/codegen/*`, which no longer exists. → **I-4**. | + +--- + +# Part IV — Improvements + +Grouped by the axes you asked about. Items marked **[P*n*]** have a corresponding diff in the patch set. + +## 4.1 Readability + +The code is already unusually well commented — the comments explain *why*, not *what*, and several of them (the `first_adjust` bitwise-vs-arithmetic note at 660–665, the `batch_invert` early-`leave` rationale at 575–579, the pairing LHS/RHS naming inversion at 3653–3665) are exactly the kind of thing that saves a reviewer an hour. Keep that. The problems are specific: + +1. **Comments that contradict the code must go.** The `0x80` claim (316–317, 347) is worse than no comment: it asserts the property that TA-5 was about, and asserts it wrongly. Interpolate `{{ memory.low_memory_scratch_start|hex() }}` instead of hardcoding. Fix the `docs/MEMORY_LAYOUT.md` path. **[P6]** +2. **Delete dead constants, or explain them.** `QUOTIENT_RETURN_MPTR`, `Q_COM_MPTR`, `G1_IDENTITY_MPTR`, `TRACE_U256_MPTR`, `X_N_MINUS_1_INV_MPTR`, the unused `r` parameter. `G1_IDENTITY_MPTR`'s comment describing non-existent `mload`s is actively misleading. If they are placeholders for future emitter modes, say so in one line each. +3. **The VM opcode table should list what is implemented.** Documenting 22 opcodes when 11 have `case` arms sends a reviewer looking for handlers that do not exist. +4. **Emitted counts must match emitted code.** Line 3023 says "43 commitment(s)" and emits 42. Whatever the resolution to L-2, the comment and the code have to agree — a generated comment that lies is a defect in the generator. **[P7]** +5. **Name the aliases.** `TRANSCRIPT_MPTR = RETURN_MPTR = QUOTIENT_RETURN_MPTR = 0x1000` and `SELECTOR_ACC_MPTR = BATCH_INV_SCRATCH_MPTR = 0xb140` are correct but invisible. Emit a short "lifetime map" comment block at each alias site stating which phase owns the window and which line the previous owner dies at. A future reader should not have to trace 2,000 lines to establish it, as I did. +6. **Mark the unreachable branch.** `load_acc_point` 830–839 is dead by construction; one sentence saying why turns a puzzle into a defence. + +## 4.2 Correctness + +Nothing found is wrong today. These are the places where correctness rests on something invisible: + +1. **Make the memory invariant self-enforcing.** One `if gt(mload(0x40), TRANSCRIPT_MPTR) { revert }`. This is the single highest value-per-byte change in the whole set. **[P2]** +2. **Pin the pragma.** `pragma solidity 0.8.30;`. The current `^0.8.24` claims compatibility that demonstrably does not exist — 0.8.24 produces a 29,567-byte runtime that cannot be deployed. **[P3]** +3. **Assert the `batch_invert` scratch capacity in the planner** rather than relying on the arithmetic landing exactly on `LAGRANGE_DENOMS_MPTR`. This one fails *silently*, which makes it worse than the others. **[P9]** +4. **Turn the two latent codegen divergences into assertions** — permutation `z_last` emission order, and column-vs-query fixed-eval counting. Both are currently "happens to be inert". +5. **Wire the declared generator errors.** `GeneratorError::RotatedInstanceQuery` and `UnsupportedInstanceColumnShape` have `Display` impls but no construction site anywhere in the reviewed tree. Either wire them or delete them — a declared-but-unreachable error reads as a guarantee that is not there. +6. **Fix `CHALLENGE_MPTR`'s aliasing** before any circuit with user-phase challenges is rendered. **[P8]** + +## 4.3 Soundness + +1. **Bind the SRS.** The build-time pairing check `e([τ]G1, G₂) == e(G1, s_g2)` closes the gap between "the generator emitted what it was given" and "what it was given is the ceremony's key". Plus `assert params.g2() == G2Affine::generator()`. **[P11]** +2. **Extend `vk_digest` coverage**, or absorb `EXPECTED_VK_CODEHASH` into the transcript, so the base points, the quotient program, the accumulator schema and the feature profile are bound rather than merely pinned. (The team's own TA-2.) +3. **Strengthen the deployment probe** with a false-pairing case and a wrong-subgroup rejection case. The current probe proves the precompiles *exist*; the accept decision needs them to be *correct*. **[P5]** +4. **Bound the quotient-VM operands.** Defence in depth today; mandatory the moment the VK stops being immutable. **[P12]** +5. **Assert the committed-instance identity invariant at codegen**, or plumb the commitment through calldata. **[P7]** + +## 4.4 Robustness + +1. **Cap the gas forwarded to validating precompile calls.** This is the change with real operational impact: it converts an attacker-chosen 30M-gas burn into a bounded ~1M. **[P1]** +2. **Custom errors for every revert class.** Five selectors, ~4 bytes of returndata each, and incident response stops being guesswork. **[P4]** +3. **Relax the `calldatasize` pin to `>=`,** or document the constraint. The other four pins already carry the security property. Today, calling through an ERC-2771 forwarder produces an empty revert that no one will diagnose quickly. +4. **Emit a `BUILD_ID` immutable.** Fleet inventory and incident scoping become mechanical instead of archaeological. **[P10]** +5. **Verify solc by SHA-256, not by version string.** **[P13]** +6. **Pin a `templates/` tree digest in CI.** This is the cheapest possible closure of the L-1 gap: it does not need the SRS, and it forces a template edit to be accompanied by an explicit fixture re-render. **[P14]** +7. **Write the incident-response section.** Wrapper-held replaceable verifier address; wrapper pause; migration means redeploy VK → verifier → repoint every wrapper; wrappers must absorb `block.chainid` and their own address into the statement. + +## 4.5 What not to change + +Some things that look like smells are correct and should be left alone: + +- **The absolute memory layout.** It is why this verifier costs 1.28M rather than 2M+. Guard it (P2); do not rewrite it to use the free-memory pointer. +- **Deferring curve checks to the precompiles.** Implementing on-curve checks for 381-bit coordinates in Yul would be far more expensive and far more error-prone than letting `G1MSM` do it. The right fix for the resulting gas behaviour is the gas cap (P1), not in-Yul validation. +- **Success-or-revert instead of returning `false`.** It is the safer default for a verifier. Just make the reverts distinguishable (P4). +- **Immutability with no admin, no pause, no upgrade.** Correct for a raw verifier. The gap is documentation of the wrapper's obligations, not the design. +- **The randomised accumulator batching.** Multiplying the two pairing equations would have been the obvious and wrong thing to do; this is the right construction, correctly ordered. +- **The VK-as-data contract with an `INVALID` prefix.** Cheap, tamper-evident, and compiler-independent to reproduce. + +--- + +# Part V — Patch set + +Diffs are against `midfall/proofs/solidity-verifier`. The generated `.sol` files are build outputs, so every fix belongs in a template or in the generator. Patches are ordered by value-per-line-changed, not by finding severity. + +Applicable diffs are in `patches/`. Where a fix needs generator logic rather than a template edit, the diff is a precise sketch with the exact insertion point named. + +| Patch | Fixes | Type | Risk | +| --- | --- | --- | --- | +| **P1** | M-2 | gas cap on validating precompile calls | low — needs a codegen-derived constant | +| **P2** | M-1 | runtime free-memory-pointer guard | trivial | +| **P3** | M-1 | pin the pragma | trivial | +| **P4** | L-3 | custom error selectors | mechanical, touches every revert site | +| **P5** | M-3 | strengthen the deployment probe | low; deploy-gas only | +| **P6** | I-1 | fix contradictory comments | trivial | +| **P7** | L-2 | committed-instance invariant + honest comment | generator assertion | +| **P8** | L-5 | un-alias `CHALLENGE_MPTR` | planner change | +| **P9** | L-7 | assert `batch_invert` scratch capacity | planner assertion | +| **P10** | L-8 | `BUILD_ID` immutable | trivial | +| **P11** | H-1 | SRS ceremony consistency check | build-time only | +| **P12** | L-6 | quotient-VM operand bounds | ~10 gas/operand | +| **P13** | M-5 | pin solc by SHA-256 | trivial | +| **P14** | L-1 | template-tree digest gate in CI | test-only | + +--- + +## P2 — runtime memory-safety guard *(do this one first)* + +Six gas, once per call, and it converts M-1 from "protected by a test in another repo" to "protected by the deployed bytecode". + +```diff +--- a/templates/contracts/Halo2Verifier.sol ++++ b/templates/contracts/Halo2Verifier.sol +@@ + assembly ("memory-safe") { ++ // The generated layout below writes absolute addresses starting at ++ // TRANSCRIPT_MPTR. The `memory-safe` annotation above is what lets ++ // solc's stack-to-memory mover reserve spill slots upward from ++ // 0x80; that reservation is compiler-version and optimiser ++ // dependent (observed 0x8c0 on 0.8.24, 0x8e0 on 0.8.26+). If it ++ // ever reaches TRANSCRIPT_MPTR, the first transcript write would ++ // clobber a live Yul local. Assert the invariant here so it is ++ // enforced by the deployed bytecode rather than by a generator-side ++ // test the integrator does not run. ++ if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. +``` + +Add the same guard at the top of `require_eip2537_precompiles`, which runs in the **creation** frame that the generator's existing guard test does not inspect. + +--- + +## P3 — pin the pragma + +`0.8.24` does not produce a deployable contract (29,567 B runtime vs the 24,576 B EIP-170 limit — verified under revm Prague). The floating pragma advertises compatibility the artefact does not have. + +```diff +--- a/templates/contracts/Halo2Verifier.sol ++++ b/templates/contracts/Halo2Verifier.sol +@@ -1,2 +1,6 @@ + // SPDX-License-Identifier: CC0-1.0 +-pragma solidity ^0.8.24; ++// Pinned, not floating. The generated layout's correctness depends on solc's ++// stack-spill reservation staying below TRANSCRIPT_MPTR, and the runtime size ++// depends on --optimize-runs. Verified: 0.8.24 emits a 29,567-byte runtime, ++// which exceeds EIP-170 and cannot be deployed. ++pragma solidity {{ template_constants.pinned_solc_version }}; +``` + +Same change in `templates/contracts/Halo2VerifyingKey.sol` and `templates/contracts/Halo2QuotientEvaluator.sol`. Emit `--optimize-runs` into a comment header at the same time, so the artefact records the setting it was built with. + +--- + +## P1 — bound the gas forwarded to validating precompile calls + +EIP-2537 `G1MSM` pricing is `k · 12000 · discount[k] / 1000` — a pure function of input length, no data dependence — so an exact constant is computable at codegen time. A 2× margin absorbs any future schedule change while still bounding the burn. + +```diff +--- a/templates/partials/verifier/Constants.sol ++++ b/templates/partials/verifier/Constants.sol +@@ ++ // Gas caps for the EIP-2537 calls that double as curve/subgroup ++ // validators. A precompile that rejects its input consumes ALL gas ++ // forwarded to it, and `staticcall(gas(), ...)` forwards 63/64 of the ++ // remainder -- so an unbounded call turns one malformed proof byte into a ++ // full-gas-limit burn for whoever pays. These are the EIP-2537 costs for ++ // the exact input lengths this verifier renders, doubled for margin. ++ uint256 internal constant G1MSM_GAS_1PAIR = {{ g1msm_gas_1pair }}; ++ uint256 internal constant G1MSM_GAS_FUSED = {{ g1msm_gas_fused }}; ++ uint256 internal constant G1ADD_GAS = {{ g1add_gas }}; ++ uint256 internal constant PAIRING_GAS_2 = {{ pairing_gas_2pair }}; +``` + +```diff +--- a/templates/partials/verifier/AccumulatorHelpers.yul ++++ b/templates/partials/verifier/AccumulatorHelpers.yul +@@ -292 +- out := staticcall(gas(), {{ ...g1msm_address|hex() }}, acc_scratch, {{ ...g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ ...g1_bytes|hex() }}) ++ out := staticcall(G1MSM_GAS_1PAIR, {{ ...g1msm_address|hex() }}, acc_scratch, {{ ...g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ ...g1_bytes|hex() }}) +@@ -397 + out := staticcall( +- gas(), ++ G1MSM_GAS_1PAIR, + {{ ...g1msm_address|hex() }}, +``` + +The same substitution is needed at the six emitter-generated sites (generated lines 3558, 3573, 3584, 3629, 3643 and the `ec_pairing` call at 622), which come from `src/lowering/kzg/mod.rs` and `templates/partials/verifier/FinalPairing.yul`. + +**Add the gas-cost model next to the plan** so the constants cannot drift from the input lengths: + +```rust +// src/lowering/plan.rs +/// EIP-2537 G1MSM gas for `k` pairs: k * 12000 * discount[k] / 1000. +fn g1msm_gas(k: usize) -> u64 { /* EIP-2537 discount table */ } + +// asserted at plan time: +assert_eq!(fused_msm_pairs * G1_MSM_PAIR_BYTES, fused_msm_input_bytes); +let g1msm_gas_fused = 2 * g1msm_gas(fused_msm_pairs); +``` + +**Caveat.** The cap must be generous and codegen-derived. A hand-tuned value that is too tight becomes a liveness bug on a chain with a different gas schedule — which is a worse failure than the griefing it prevents. If you would rather not take that risk, the weaker alternative is to document the behaviour loudly in the NatSpec so integrators know to bound the gas they hand the verifier themselves. + +--- + +## P5 — strengthen the deployment probe + +Currently `G1ADD` gets a known-answer vector and the two precompiles that decide acceptance get identity inputs only. Add three probes; all are constant-size and cost deployment gas only. + +```diff +--- a/templates/partials/verifier/PrecompileSmoke.sol ++++ b/templates/partials/verifier/PrecompileSmoke.sol +@@ (after the existing G1ADD known-answer block) ++ // Known-answer G1MSM: [2]*G == 2G. The identity-input probe below ++ // is satisfied by any implementation that echoes zeros; this one ++ // is not. G1MSM is the verifier's subgroup validator for every ++ // absorbed proof commitment, so a wrong G1MSM is a soundness bug, ++ // not a liveness bug. ++ mstore(add(scratch, 0x00), {{ g1_gen_x_hi }}) ++ mstore(add(scratch, 0x20), {{ g1_gen_x_lo }}) ++ mstore(add(scratch, 0x40), {{ g1_gen_y_hi }}) ++ mstore(add(scratch, 0x60), {{ g1_gen_y_lo }}) ++ mstore(add(scratch, 0x80), 2) ++ if iszero(staticcall(gas(), 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } ++ if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } ++ if iszero(and( ++ and(eq(mload(add(scratch, 0x00)), {{ g1_two_x_hi }}), ++ eq(mload(add(scratch, 0x20)), {{ g1_two_x_lo }})), ++ and(eq(mload(add(scratch, 0x40)), {{ g1_two_y_hi }}), ++ eq(mload(add(scratch, 0x60)), {{ g1_two_y_lo }})) ++ )) { revert(0, 0) } ++ ++ // Negative G1MSM probe: a point that is on the curve but NOT in ++ // the r-order subgroup must make the precompile fail. This is the ++ // one property the whole deferred-validation strategy rests on and ++ // the one property no existing probe tests. ++ mstore(add(scratch, 0x00), {{ g1_wrong_subgroup_x_hi }}) ++ mstore(add(scratch, 0x20), {{ g1_wrong_subgroup_x_lo }}) ++ mstore(add(scratch, 0x40), {{ g1_wrong_subgroup_y_hi }}) ++ mstore(add(scratch, 0x60), {{ g1_wrong_subgroup_y_lo }}) ++ mstore(add(scratch, 0x80), 1) ++ if staticcall(gas(), 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } ++ ++ // Known-FALSE pairing: e(G, G2) * e(G, G2) != 1. The identity probe ++ // below only proves the precompile can say "true"; the verifier's ++ // entire accept decision is `PAIRING_CHECK(...) == 1`, so an ++ // implementation that always returns 1 must be caught here. ++ // ... build the two-pair input (G, G2), (G, G2) ... ++ if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } ++ if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } ++ if iszero(iszero(mload(scratch))) { revert(0, 0) } // must be 0 ++ ++ // Known-TRUE pairing: e(G, G2) * e(-G, G2) == 1. ++ // ... build the two-pair input (G, G2), (-G, G2) ... ++ if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } ++ if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } ++ if iszero(eq(mload(scratch), 1)) { revert(0, 0) } +``` + +The generator already has `G1_BASE` and `G2_BASE`, so the generator/two-G constants are free. A wrong-subgroup point can be produced once, offline, and baked in as a constant (any point on E(Fp) with order ≠ r — e.g. map a fixed seed into E(Fp) and skip cofactor clearing). + +Also: give `AUDIT.md` TA-6 a `Status:` line and re-rate it. With the accept path resting entirely on `0x0f`, Low is too low. + +--- + +## P11 — bind the trusted setup at build time *(highest-severity fix)* + +The generator already runs the analogous check for G1 and explains why. Extend it to G2 and to τ. + +```diff +--- a/src/lowering/vk.rs ++++ b/src/lowering/vk.rs +@@ + let g1_pt: G1Affine = G1Affine::generator(); + let g2_pt: G2Affine = self.params.g2().to_affine(); + let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); ++ ++ // The G1 base is validated above by reconstructing it from ++ // g_lagrange. Do the same work on the G2 side, which is currently ++ // taken on trust from `params` and is the element every soundness ++ // guarantee rests on. ++ // ++ // 1. G2_BASE must be the canonical generator, for the same reason ++ // G1_BASE must be: the emitted pairing equation assumes it. ++ assert_eq!( ++ g2_pt, ++ G2Affine::generator(), ++ "SRS G2 base is not the canonical BLS12-381 generator; the \ ++ emitted pairing equation would not be the KZG identity" ++ ); ++ ++ // 2. NEG_S_G2 must be the negation of [tau]G2 for the SAME tau ++ // that produced g_lagrange. Without this the generator will ++ // happily emit a verifier keyed to an SRS whose toxic waste the ++ // submitter knows -- and every other control in this repo ++ // (certification, codehash pinning, trace differential, replay ++ // fixture) checks self-consistency only, so all of them pass. ++ // ++ // Commit f(X) = X in the Lagrange basis: [tau]G1 = sum_i w^i L_i. ++ let omega = self.vk.get_domain().get_omega(); ++ let mut w = ::ONE; ++ let tau_g1 = g_lagrange ++ .iter() ++ .map(|g| { let t = *g * w; w *= omega; t }) ++ .fold(G1Projective::identity(), |acc, t| acc + t) ++ .to_affine(); ++ assert!( ++ bls12_381::pairing_check(&[ ++ (&tau_g1, &G2Affine::generator()), ++ (&(-G1Affine::generator()), &self.params.s_g2().to_affine()), ++ ]), ++ "SRS inconsistency: s_g2 does not correspond to the tau that \ ++ generated g_lagrange; NEG_S_G2_BASE would be emitted from an \ ++ SRS unrelated to the commitment basis" ++ ); +``` + +Additionally, and outside the code: pin the SRS by SHA-256 in the generator, fail the render on mismatch, and record the hash plus the ceremony transcript reference in the artefact manifest. `CODEGEN_ASSURANCE_DOSSIER.md:46` already lists this as a required record; it is simply not being produced. + +--- + +## P10 — on-chain build identity + +```diff +--- a/templates/partials/verifier/Constants.sol ++++ b/templates/partials/verifier/Constants.sol +@@ ++ /// @notice Identifies the exact build that produced this verifier. ++ /// @dev keccak256(generator_commit || feature_list || vk_digest || ++ /// EXPECTED_VK_CODEHASH || srs_sha256). Nothing else in the deployed ++ /// runtime identifies the codegen, the feature profile, or the SRS ++ /// (CBOR metadata is stripped), so during an incident this is the ++ /// only way to answer "which of our deployments has the affected ++ /// codegen?" from chain state alone. ++ bytes32 public immutable BUILD_ID = {{ build_id }}; +``` + +Zero cost on the verify path. Publish the preimage components alongside the deployment record. + +--- + +## P4 — custom errors + +Every revert is currently `revert(0, 0)`. Writing a selector at `0x00` is legitimate scratch use and does not disturb the generated layout (which starts at `0x1000`). + +```yul +// helper, emitted once in AssemblyHelpers.yul +function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) +} +``` + +Minimum useful taxonomy: + +| Selector | Meaning | Current sites | +| --- | --- | --- | +| `BadCalldataShape()` | ABI heads, proof length, instance count, `calldatasize` | 338, 1048 | +| `VkMismatch()` | `extcodesize` / `extcodehash` / header cross-check | 1007, 1024 | +| `NonCanonicalScalar()` | instance, eval or q_eval ≥ r | 1133, 1316, 1369, 1399 | +| `BadPointEncoding()` | EIP-2537 pad, coordinate ≥ p, accumulator packing | 439, 440, 445, 448, 1064 | +| `PrecompileFailed()` | any precompile success/returndatasize failure | 212, 224, 273, 284, 610, 1482 | +| `ProofRejected()` | the final pairing | 3666, 3678 | +| `QuotientProgramInvalid()` | VM structural post-conditions | 2620, 2654, 2661, 2668 | + +Declare them in the contract so the ABI carries them and off-chain tooling can decode. + +--- + +## P7 — make the committed-instance invariant explicit + +Two changes, one in the generator, one in the emitted comment. + +```rust +// src/lowering/encoding/mod.rs -- where committed-instance commitments are pinned +assert!( + committed_instance_comms.iter().all(|c| *c == G1_IDENTITY_MPTR), + "committed-instance commitments are pinned to the G1 identity; a \ + non-identity commitment would require a calldata channel and an MSM \ + term that this emitter does not produce" +); +``` + +```rust +// src/lowering/kzg/mod.rs -- per-set MSM emission +// Emitted x1 exponents must be {0..m-1} minus exactly the indices of +// commitments proven to be the identity. Anything else means a real +// commitment was dropped from the batch while its eval stayed in q_eval_set. +let emitted: BTreeSet = /* exponents actually emitted */; +let omitted: BTreeSet = (0..m).collect::>() + .difference(&emitted).copied().collect(); +assert_eq!(omitted, identity_commitment_indices, + "point set {s}: MSM omits x1 exponents {omitted:?} but only \ + {identity_commitment_indices:?} are identity commitments"); +``` + +And fix the emitted comment so it stops lying: + +```diff +- // q_eval_set[0]: 43 commitment(s) ++ // q_eval_set[0]: 43 evaluation terms, 42 commitment terms. ++ // x1^1 is omitted because commitment 1 (the committed ++ // instance) is pinned to the G1 identity and contributes ++ // nothing to the MSM. The KZG batch still constrains its ++ // evaluation to zero. Asserted in kzg/mod.rs. +``` + +--- + +## P8 — un-alias `CHALLENGE_MPTR` + +```diff +--- a/templates/partials/verifier/Constants.sol ++++ b/templates/partials/verifier/Constants.sol +@@ +- uint256 internal constant CHALLENGE_MPTR = {{ memory.challenge_mptr }}; ++ // User-phase challenge window. MUST NOT overlap the named challenge slots ++ // below: TranscriptProofParser squeezes phase challenges to ++ // CHALLENGE_MPTR + 32*i and then unconditionally squeezes theta to ++ // THETA_MPTR. If the windows alias, phase-1 challenge 0 is silently ++ // overwritten by theta. ++ uint256 internal constant CHALLENGE_MPTR = {{ memory.user_challenge_mptr }}; +``` + +with a planner assertion: + +```rust +assert!( + memory.user_challenge_mptr + 32 * num_user_challenges <= memory.theta_mptr, + "user-phase challenge window overlaps the named challenge slots" +); +``` + +--- + +## P9 — assert `batch_invert` scratch capacity + +The current arithmetic lands *exactly* on `LAGRANGE_DENOMS_MPTR` with zero slack, and overflowing it corrupts silently — the modexp still succeeds, so nothing reverts. + +```rust +// src/lowering/layout/memory.rs, where BATCH_INV_SCRATCH is sized +// Forward pass writes (n-2) prefix products, then a 0xc0-byte EIP-198 frame. +let required = (n.saturating_sub(2)) * 32 + 0xc0; +assert!( + batch_inv_scratch_len >= required, + "batch_invert scratch is {batch_inv_scratch_len} bytes but needs \ + {required} for n={n}; overflow silently corrupts denominator[0] and the \ + backward pass returns wrong inverses WITHOUT reverting" +); +``` + +--- + +## P12 — bound the quotient-VM operands + +Defence in depth while the VK is immutable; mandatory if it ever stops being. + +```diff +--- a/templates/partials/quotient_numerator/QuotientHelpers.yul ++++ b/templates/partials/quotient_numerator/QuotientHelpers.yul +@@ (at each operand decode) + let q_ptr := shr(240, mload(q_pc)) ++ // Operands are raw memory addresses from the pinned program. The ++ // codehash pin is the only thing standing between a malformed ++ // program and arbitrary memory access; clamp anyway so the ++ // property is local to this file. ++ if or(lt(q_ptr, {{ memory.reversed_evals_mptr|hex() }}), ++ gt(q_ptr, {{ memory.vm_operand_max|hex() }})) { revert(0, 0) } +@@ (selector bucket write) ++ if iszero(lt(q_sel_idx, {{ num_simple_selectors }})) { revert(0, 0) } ++ if iszero(lt(q_sel_gap, {{ num_identities }})) { revert(0, 0) } +@@ (stack pop) ++ if lt(q_sp, {{ memory.vm_stack_floor|hex() }}) { revert(0, 0) } + q_sp := sub(q_sp, 0x20) +``` + +Note the existing terminal check `eq(q_sp, 0xb8e0)` does **not** catch a balanced underflow (pop-then-push restores the pointer), which is why the floor check is needed at the pop site. + +--- + +## P13 — pin solc by content hash + +```diff +--- a/scripts/install_pinned_solc.sh ++++ b/scripts/install_pinned_solc.sh +@@ ++# Content hash, not version string. `--version` output is trivially forged by a ++# substituted binary, and this project's entire reproducibility claim rests on ++# the compiler being exactly this one. ++PINNED_SOLC_SHA256="" ++ + if [[ ! -x "$solc_path" ]] || ! "$solc_path" --version | grep -q "Version: ${PINNED_SOLC_VERSION}"; then + url="https://binaries.soliditylang.org/${platform}/${binary}" + echo "[install-solc] downloading $url" + curl -fsSL "$url" -o "$solc_path" ++ actual="$(sha256sum "$solc_path" | cut -d' ' -f1)" ++ if [[ "$actual" != "$PINNED_SOLC_SHA256" ]]; then ++ echo "[install-solc] SHA-256 mismatch: got $actual, expected $PINNED_SOLC_SHA256" >&2 ++ rm -f "$solc_path" ++ exit 1 ++ fi + chmod +x "$solc_path" + fi +``` + +Also fix the `Darwin-arm64 → macosx-amd64` mapping, or record explicitly that Apple Silicon runs an x86 binary under Rosetta and is therefore not the CI compiler. + +--- + +## P14 — gate template edits in CI + +The cheapest closure of L-1. Needs no SRS, so it runs in the default CI job. + +```rust +// tests/template_digest.rs +/// The replay fixtures compile committed pre-rendered .sol, so a template edit +/// changes what deploys without changing what any SRS-free test compiles. Pin +/// the template tree so an edit must be accompanied by a fixture re-render and +/// a README commit-stamp bump. +#[test] +fn template_tree_digest_is_pinned() { + const EXPECTED: &str = ""; + assert_eq!(hash_dir("templates/"), EXPECTED, + "templates/ changed. Re-render the fixtures (see \ + fixtures/*/README.md), update the source-commit stamps, then update \ + this digest."); +} +``` + +--- + +## Patch validation + +P2 and P3 were applied to the generated `Halo2Verifier.sol`, recompiled with the pinned toolchain and re-run through the full test suite: + +| | baseline | with P2 + P3 | delta | +| --- | --- | --- | --- | +| runtime size | 21,286 B | 21,299 B | **+13 B** | +| deployment gas | 5,330,806 | 5,333,769 | +2,963 | +| valid-proof gas | 1,279,482 | 1,279,513 | **+31** | +| test outcomes (32 cases) | 1 accept / 31 reject | identical | — | + +Thirty-one gas and thirteen bytes to move the memory invariant from "enforced by a test in another repository" into the deployed bytecode. The other patches were not compiled, since they require generator changes. + +--- + +## Suggested sequencing + +1. **P3, P2, P6** — one afternoon, no behavioural risk, closes the compiler-configuration exposure and stops the artefact from shipping comments that contradict it. +2. **P11** — the only High. Build-time only, no on-chain change, no gas cost. +3. **P5, P1** — deployment assurance and the gas-griefing bound. P1 needs the gas model, so it is the longest of the group. +4. **P4, P10, P13, P14** — operational quality: diagnosable failures, fleet inventory, supply-chain pinning, template gating. +5. **P7, P8, P9, P12** — generator hardening. None affect the current artefact's behaviour; all convert "happens to be correct" into "asserted correct". + +--- + +# Part VI — Operational readiness checklist + +Before production deployment, a deployer should be able to produce evidence for every line. Items marked ✅ were verified during this review for this artefact; the rest are deployment-specific. + +**Trusted setup** + +1. Name the ceremony, its transcript URL, and the SHA-256 of the exact SRS file used for this render. Record all three in the manifest. +2. Independently recompute `−[τ]G₂` from the ceremony's published τ point and confirm it equals `NEG_S_G2_BASE` (VK words 23–30). Do not accept the generator's output as its own evidence. +3. ✅ `G2_BASE` (words 15–22) is the canonical BLS12-381 G2 generator and `G1_BASE` (words 11–14) the canonical G1 generator. +4. Run the build-time pairing check `e([τ]G1, G₂) == e(G1, s_g2)` (**P11**) before generating anything you intend to deploy. + +**Build reproducibility** + +5. Install solc by SHA-256, not by version string. Record the hash. +6. Record the complete flag set **including `--optimize-runs`**; confirm `SOLC_OPTIMIZE_RUNS` is unset in the build environment. ⚠️ At `runs=100000` the runtime exceeds EIP-170 and will not deploy. +7. Recompile from the pinned generator commit and confirm the verifier source hash matches the deployed source byte-for-byte. +8. Confirm the generator commit, Midfall revision and Cargo feature list are one consistent triple, and record the exact feature flags — `truncated-challenges`, `outer-fewer-point-sets`, `outer-single-h-commitment` — that the prover must match. + +**Key / VK binding** + +9. ✅ `keccak256(0xfe ‖ VK payload) = 0xe68d8936…cf52`, length 17,025 — recomputed from source and confirmed against `EXPECTED_VK_CODEHASH` / `EXPECTED_VK_LENGTH`. +10. Deploy VK first, then `Halo2Verifier(vkAddress)`. Confirm `AUTHORIZED_VK()` returns the intended address. ✅ A 1-bit-mutated VK and an EOA address both revert the constructor. +11. ✅ VK header words match the circuit: `num_instances=19`, `k=20`, `has_accumulator=1`, `acc_offset=11`, `num_acc_limbs=7`, `num_acc_limb_bits=56`. +12. ✅ Domain self-consistency: `n_inv · 2ᵏ ≡ 1`; `omega` of exact order `2ᵏ`; `omega · omega_inv ≡ 1`; `omega_inv_to_l = omega_inv^|rotation_last|`. + +**Chain** + +13. ✅ The target chain must be at Prague/Pectra with EIP-2537 at `0x0b`/`0x0c`/`0x0f` **and** MCOPY (Cancun). Pre-fork deployment reverts in the constructor — verified fail-closed under `CANCUN`, `SHANGHAI` and `MERGE`. +14. Do **not** rely on `require_eip2537_precompiles()` as a correctness gate (**M-3**). Independently run against the target chain: a true pairing, a **false** pairing, a non-identity `G1ADD`, a non-identity `G1MSM`, an off-curve G1 rejection, and a wrong-subgroup G1 rejection. +15. Confirm the deployment transaction supplies enough gas for the 78-pair (`0x30c0`-byte) G1MSM probe. ✅ Deployment cost 5,330,806 gas under revm Prague. + +**Application layer** + +16. The wrapper must hold the verifier behind a **replaceable** address and must have its own pause. There is no on-chain recovery in the verifier. +17. The wrapper must bind `block.chainid`, its own address, program/state identifiers, nullifiers and freshness into the statement. A proof valid here is valid on every chain and every deployment of this bytecode. +18. Record that `verifyProof` returns `true` or reverts — never `false`. Callers must not treat a failed `staticcall` as a decodable negative result. +19. ⚠️ The verifier cannot be called through ERC-2771 forwarders or any relayer that appends calldata (**L-4**), and a malformed point burns ~63/64 of the gas you hand it (**M-2**). Bound the gas passed to `verifyProof` from the wrapper. + +**Post-deployment** + +20. Publish the deployed runtime bytecode and its keccak for both contracts, plus the full manifest from steps 1–8. Nothing on-chain identifies the build (**L-8**), so this record is the only provenance that will exist. +21. Maintain an inventory mapping each deployed address to its generator commit, feature profile, SRS hash and circuit. Without a `BUILD_ID` immutable this cannot be reconstructed from chain state during an incident. + +--- + +# Appendix A — Empirical test log + +**Environment.** solc 0.8.30 (`--optimize --optimize-runs=1 --via-ir --evm-version cancun`, CBOR metadata off) → `revm 19`, `SpecId::PRAGUE`, `blst` feature. VK deployed with a 17,025-byte runtime and codehash `0xe68d8936…cf52`, matching `EXPECTED_VK_CODEHASH` exactly. Verifier deploy cost 5,330,806 gas. + +**Baseline.** The unmodified fixture calldata (8,516 bytes) verifies in **1,279,482 gas** — reproducing the figure in `fixtures/moonlight-wrap/README.md` exactly, which independently confirms the artefact, the fixture and the compiler settings are mutually consistent. + +**40 adversarial cases. 1 accepted (the valid proof), 39 rejected.** + +| # | Mutation | Result | Gas | +| --- | --- | --- | --- | +| P1 | unmodified fixture | **ACCEPT** | 1,279,482 | +| N1 | `advice[0].x_lo` last byte flipped | reject | 492,197,920 ⚠️ | +| N2 | `perm_z[0].x_lo` last byte flipped | reject | 492,197,920 ⚠️ | +| N3 | `quotient_limb[0].x_lo` flipped | reject | 492,197,920 ⚠️ | +| N4 | `eval[0]` (committed instance) + 1 | reject | 1,279,485 | +| N5 | `eval[50]` + 1 | reject | 1,279,473 | +| N6 | `f_com.x_lo` flipped | reject | 492,197,920 ⚠️ | +| N7 | `q_eval[0]` + 1 | reject | 1,279,473 | +| N8 | `pi.x_lo` flipped | reject | 492,205,857 ⚠️ | +| N9 | `eval[0]` set to 0 | ACCEPT — *no-op; fixture value is already 0* | 1,279,482 | +| N10 | `eval[0] = r` | reject | 316,790 | +| N11 | `q_eval[0] = r` | reject | 315,830 | +| N12 | `instance[0] = r` | reject | 316,760 | +| N13 | `instance[0] = r + 1` | reject | 316,760 | +| N14 | `instance[0]` + 1 | reject | 1,279,473 | +| N15 | `instance[10]` + 1 | reject | 1,279,473 | +| N16 | `instance[11]` + 1 (acc lhs) | reject | 492,197,436 ⚠️ | +| N17 | `instance[18]` + 1 (acc rhs) | reject | 492,190,350 ⚠️ | +| N18 | `advice[0].x_hi` top byte nonzero (bad pad) | reject | 315,950 | +| N19 | `advice[0].y_hi` top byte nonzero (bad pad) | reject | 315,950 | +| N20 | `advice[0].x = p` (coordinate = modulus) | reject | 315,920 | +| N21 | `advice[0] = (1,1)` off-curve | reject | 492,197,902 ⚠️ | +| N22 | `advice[0] = (0,0)` identity | reject *(valid encoding; fails at the pairing)* | 1,278,321 | +| N23 | calldata truncated by 32 B | reject | 315,000 | +| N24 | calldata + 32 trailing zero bytes | reject | 316,240 | +| N25 | calldata + 20 bytes (ERC-2771 style) | reject | 316,720 | +| N26 | ABI head[0] `0x40 → 0x60` | reject | 315,920 | +| N28 | proof length `0x1e60 → 0x1e5f` | reject | 315,920 | +| N29 | instance count `19 → 18` | reject | 315,920 | +| N30 | instance count `19 → 20` | reject | 315,920 | +| N31 | wrong function selector | reject | 315,920 | +| D1 | ABI head[1] `0x1ec0 → 0x1f00` | reject | 315,890 | +| D2 | ABI head[1] `0x1ec0 → 0x1ea0` | reject | 315,920 | +| D3 | ABI head[0] `0x40 → 0x20` | reject | 315,920 | +| D4 | proof length `0x1e60 → 0x1e80` | reject | 315,920 | +| E1 | both accumulator points = canonical identity encoding | reject | 1,265,123 | +| E2 | acc lhs = `p−1`/`p−1` without ID flag (decodes to `(0,0)`) | reject | 315,980 | +| E3 | acc lhs word0 bit ≥ 224 set (packing violation) | reject | 315,950 | +| E4 | acc lhs/rhs swapped | reject | 1,279,473 | + +*(N27 was a no-op — the mutation coincided with the original byte — and is superseded by D1/D2.)* + +**Deployment-time cases** + +| Case | Result | +| --- | --- | +| Deploy under `SpecId::CANCUN` / `SHANGHAI` / `MERGE` | constructor reverts — fail closed ✅ | +| Verifier pointed at a 1-bit-mutated VK | constructor reverts — codehash pin holds ✅ | +| Verifier pointed at an EOA address | constructor reverts ✅ | +| solc 0.8.24, runs=1 (29,567 B runtime) | `HALT CreateContractSizeLimit` ⚠️ | +| solc 0.8.30, runs=100000 (29,836 B runtime) | `HALT CreateContractSizeLimit` ⚠️ | +| solc 0.8.30, runs=1 (21,286 B runtime) | deploys ✅ | + +**What the log establishes** + +- Three distinct rejection gas signatures — ~316k structural, ~1.28M cryptographic, and ~63/64-of-limit for curve-validity failures. The third is **M-2**, and it is not a rounding artefact: it scales exactly with the supplied limit (1.98M of 2M; 29.5M of 30M; 492M of 500M). +- The committed-instance evaluation is pinned to zero by the KZG batch (N4 rejects, fixture value is 0), which empirically settles the **L-2** analysis: the omitted `x1^1` MSM term is a gas optimisation, not a soundness gap. +- Every ABI-envelope mutation is caught cheaply, before any curve work — the calldata parser has no zero-fill or relocation hole. +- The identity encoding is accepted as a *valid* G1 encoding (N22) but does not shortcut the pairing. +- The accumulator codec rejects non-canonical and packing-violating encodings early (E2, E3, 315–316k) and rejects a plausible-looking canonical identity substitution at the pairing (E1) — the accumulator instances are bound by the proof, so they cannot be swapped for the trivial case. + +--- + +# Appendix B — Verified-correct inventory + +Recording what was checked and found sound, so the report distinguishes "verified" from "not looked at". + +**Fiat–Shamir** +- The VK is bound: `vk_digest` is the first absorbed word, and `transcript_repr` covers the constraint system, all fixed commitments and all permutation commitments. No weak-Fiat-Shamir / "Frozen Heart". +- Absorb-before-squeeze ordering matches `src/plonk/verifier.rs` and `src/poly/kzg/mod.rs` exactly. Every prover-controlled value is absorbed strictly before any challenge depending on it; `π` is last and no challenge depends on it. +- Reseed semantics match Rust; back-to-back squeezes (β/γ, x1/x2) produce distinct challenges. +- No value used in the identity is unabsorbed: all 1,552 VM operand pointers and all 141 raw `mload`s in the identity code resolve into the absorbed-evaluation window `[0x9480, 0xa140)`. Zero point into uninitialised memory. +- No challenge can be influenced after derivation: slots `0x7900…0x7a40` are never rewritten except the intentional `x3` mask, which mirrors Rust. Verified by an exhaustive scan of every constant-address `mstore`/`mcopy`/`calldatacopy` destination. + +**Field elements** +- All 19 instances, 102 evaluations and 5 q_evals are checked `lt(v, r)` — so `v = r` is rejected, not reduced. +- No Fq/Fr confusion: every `addmod`/`mulmod` uses `FR_MODULUS`. Base-field comparisons use dedicated split `BLS_P_HI` / `BLS_P_MINUS_ONE_LO` constants, verified equal to the top 16 / bottom 32 bytes of `p−1`. +- The transcript absorbs the *same* word that was range-checked — no absorb/use divergence. + +**Curve points** +- EIP-2537 padding enforced for every proof G1; malformed encodings revert rather than being normalised (which would create transcript aliases). +- All 31 proof commitments plus `f_com` appear exactly once in the 78-pair fused G1MSM; `π` passes through its own G1MSM; both accumulator points are forced through G1MSM. **Every prover-supplied G1 reaches a subgroup-checking precompile.** +- No prover-supplied point reaches `0x0b` G1ADD (which does not subgroup-check) without prior validation. +- All 45 VK commitments are on-curve and in the r-subgroup; none of the 10 simple-selector commitments is the identity. + +**Accumulator** +- The limb decomposition is canonical and bijective; the coordinate is bounded by `p−1` and `hi < 2¹²⁸`; the identity has exactly one accepted encoding; a decoded `(0,0)` outside the sentinel is rejected; `PACKED_0_WITH_ID_FLAG − PACKED_0 = 2⁵⁶` exactly, with no carry. +- `acc_offset` is fixed at 11 and `11 + 2·2·2 = 19` matches the pinned instance count — not attacker-shiftable. +- The decoded accumulator is not decorative: it is folded into the pairing inputs before `ec_pairing`. +- The randomised batching is sound: `alpha` is drawn over exactly `0x220` bytes covering all four already-final points, with a zero-draw guard. + +**MSM and linear combinations** +- Exactly 49 identities, positions 0–48, each consuming one `y` step; order matches `partially_evaluate_identities`. All 21 in-bytecode selector gaps and 10 rendered tails are arithmetically correct — every bucket lands on `Σ y^(48−j)·eval_j`. No two identities share a coefficient. +- All 102 evaluations are opened exactly once across the 5 point sets (42+6+6+33+15 = 102). None missing, none double-counted. +- Truncated `x1`/`x4` powers are used identically on the commitment and evaluation sides. +- The 78 `(point, scalar)` pairs are written contiguously over exactly `0x30c0` bytes — no stale memory in the MSM buffer. +- `batch_invert` fails closed on zero and on non-canonical input, and correctly `leave`s before the backward pass on a failed modexp. Algorithm hand-verified for n = 2, 3, 4, 30. + +**Pairing** +- `ec_pairing` checks staticcall success, `returndatasize() == 0x20`, and `mload == 1` (strict), in that order — a short or empty return cannot be read as success. Input is `0x300` bytes = exactly 2 pairs. +- Orientation is the standard KZG identity at `x3`. + +**EVM level** +- Calldata bounds: four independent pins executed before any data-dependent read; ABI offsets cannot be attacker-chosen; proof section lengths sum to `0x1e60` exactly and the terminal cursor check re-proves it dynamically. +- VK pinning: `extcodesize` **and** `extcodehash` re-checked on every proof, before `extcodecopy`; the pinned hash covers the `INVALID` prefix (recomputed); no deploy-order footgun. +- Memory: all three address aliases are lifetime-disjoint reuse, proven by tracing the write map in program order. **No write anywhere below `0x1000`** — Solidity's scratch, free-memory pointer and zero slot are never touched. No write into the VK region after `extcodecopy`. Every constant-address `mload` reads a written region. +- Precompiles: all 13 call sites capture success, check `returndatasize()`, and are ultimately enforced. Input lengths are correct for the EIP-2537 ABI throughout. Input/output buffer aliasing is safe. +- Control flow: `success` is initialised once and every accumulation is converted to a revert at a section boundary. There is no path reaching the terminal `return` without the pairing having passed. `verifyProof` is correctly `view`. +- Arithmetic: no `signextend`/`sar`/`sdiv`/`smod`/`slt`/`sgt` anywhere; every `div`/`mod` has a nonzero constant or `r` as divisor; `scalar_inv` rejects `0` and `≥ r`, closing the "modexp returns 0 for `x ≡ 0 mod r`" alias; the accumulator's `sub(packed, first_adjust)` cannot underflow. +- Quotient VM: structural post-conditions fail closed on over-read, live operands and dropped stack entries; unknown opcodes revert; the constant-table index maximum is 177, exactly filling the 178-word reservation. +- Gas/DoS: every loop bound is a codegen constant or comes from the codehash-pinned VK; calldata size is pinned; peak memory is a constant `0xe340`. No attacker lever makes the cost depend on proof *contents* — **except** the precompile-failure path in M-2. + +**Documentation accuracy** +- `MEMORY_LAYOUT.md`'s theta-relative offset table matches the artefact exactly for all 26 entries. +- `HALO2_MIDNIGHT_VERIFIER_SPEC.md` §6's VK word layout matches the VK source word-for-word. +- `fixtures/moonlight-wrap/calldata.bin` is 8,516 bytes = `INSTANCE_CPTR (0x1ee4) + 19·32`, and `proof_len = 0x1ec4 − 0x64 = 7,776` — both exactly as the artefact's constants require. +- Selector `0x1e8e1e13 = keccak("verifyProof(bytes,uint256[])")[0:4]` ✅. +- Documentation drift is confined to I-1 (the `0x80` comments) and I-4 (stale `src/codegen/*` anchors in the audit chain). + + +--- + +*Review performed 12 August 2026 against `fixtures/moonlight-wrap` (`Halo2Verifier.sol` `3861a403…`, `Halo2VerifyingKey.sol` `ec94cabe…`). Static analysis was cross-checked by execution under revm 19 / `SpecId::PRAGUE` with solc 0.8.30. Items in §2.5 and Appendix B's caveats mark what could not be verified from the material available — in particular the trusted setup (H-1), the `vk_digest` preimage, the arithmetic content of the quotient bytecode, and the accumulator's orientation contract with the producing circuit.* diff --git a/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md b/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md index fb9739587..c72805d98 100644 --- a/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md +++ b/proofs/solidity-verifier/docs/audit/REVIEW_PACKET.md @@ -64,11 +64,11 @@ Fill this table for the exact artifact under review. Use | Item | Value | | --- | --- | -| Repository commit | `a096e71746e401404f250817ca4e857bac1eef56` | +| Repository commit | `a096e71746e401404f250817ca4e857bac1eef56` (**this repository** at packet creation; the Midfall dependency revision is a separate stamp — see the provenance-identities table in `docs/reference/REPRODUCIBLE_BUILDS.md`) | | Working tree status at packet creation | clean before this packet was added | | Rust toolchain | `rust-toolchain.toml` | -| Solidity compiler | `solc 0.8.30+commit.73712a01` | -| Solidity flags | `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` | +| Solidity compiler | `solc 0.8.30+commit.73712a01`, SHA-256-pinned by `scripts/install_pinned_solc.sh` | +| Solidity flags | `--bin --optimize --optimize-runs --via-ir --evm-version cancun --no-cbor-metadata` — record `` per artifact; it changes both bytecode and deployability (0.8.30 at `runs=100000` exceeds EIP-170) | | Cargo features | fill per artifact | | Generated verifier source hash | fill per artifact | | Generated VK source hash | fill per artifact, if split | @@ -80,7 +80,9 @@ Fill this table for the exact artifact under review. Use | Public input fixture hash | fill per artifact | | Calldata hash | fill per artifact | -The currently recorded IVC benchmark manifests live in +Generate the per-artifact rows with `scripts/generate_artifact_manifest.sh +`; it emits this table for each rendered fixture dump under +`target/`. The currently recorded IVC benchmark manifests live in `docs/reference/REPRODUCIBLE_BUILDS.md`. ## 5. Reading Order diff --git a/proofs/solidity-verifier/docs/audit/patches/P11_srs_binding.patch b/proofs/solidity-verifier/docs/audit/patches/P11_srs_binding.patch new file mode 100644 index 000000000..7f25cad83 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/patches/P11_srs_binding.patch @@ -0,0 +1,63 @@ +--- a/src/lowering/vk.rs ++++ b/src/lowering/vk.rs +@@ -93,6 +93,60 @@ + let g1_pt: G1Affine = G1Affine::generator(); + let g2_pt: G2Affine = self.params.g2().to_affine(); + let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); ++ ++ // The G1 base is validated above by reconstructing it from ++ // `g_lagrange`. Do the same work on the G2 side. Until now ++ // `g2` and `s_g2` were taken on trust from `params`, and ++ // `NEG_S_G2_BASE` is the element every soundness guarantee in the ++ // deployed verifier rests on: the final pairing is ++ // e(final_com - v*G + x3*pi, G2_BASE) * e(pi, NEG_S_G2_BASE) == 1 ++ // and anyone who knows the tau behind NEG_S_G2_BASE can forge an ++ // opening for any statement. ++ // ++ // Every other control in this repository -- quotient ++ // certification, the dual build, generator invariants, the VK ++ // codehash pin, the trace differential, the replay fixtures -- ++ // checks SELF-CONSISTENCY. An SRS substituted at build time ++ // produces a perfectly self-consistent artifact, so all of them ++ // pass. This assertion is the only place that can catch it. ++ ++ // 1. G2_BASE must be the canonical generator, for the same reason ++ // G1_BASE must be: the emitted equation assumes it. ++ assert_eq!( ++ g2_pt, ++ G2Affine::generator(), ++ "SRS G2 base is not the canonical BLS12-381 G2 generator; the \ ++ emitted pairing equation would not be the KZG identity" ++ ); ++ ++ // 2. `s_g2` must correspond to the SAME tau that produced ++ // `g_lagrange`. Commit f(X) = X in the Lagrange basis to obtain ++ // [tau]G1 = sum_i omega^i * L_i, then check the pairing ++ // e([tau]G1, G2) == e(G1, s_g2). This ties the G2 side of the ++ // key to the commitment basis the prover actually uses. ++ let omega = self.vk.get_domain().get_omega(); ++ let mut w = ::ONE; ++ let tau_g1 = g_lagrange ++ .iter() ++ .map(|g| { ++ let term = *g * w; ++ w *= omega; ++ term ++ }) ++ .fold(G1Projective::identity(), |acc, t| acc + t) ++ .to_affine(); ++ assert!( ++ srs_tau_is_consistent(&tau_g1, &self.params.s_g2().to_affine()), ++ "SRS inconsistency: s_g2 does not correspond to the tau that \ ++ generated g_lagrange. NEG_S_G2_BASE would be emitted from an \ ++ SRS unrelated to the commitment basis -- i.e. from a key whose \ ++ toxic waste may be known to whoever supplied it." ++ ); ++ ++ // TODO(deployment): additionally pin the SRS asset by SHA-256 here ++ // and record it, plus the ceremony transcript reference, in the ++ // artifact manifest. CODEGEN_ASSURANCE_DOSSIER.md already lists ++ // this as a required record; nothing currently produces it. + let g1 = g1_to_u256s(g1_pt); + let g2 = g2_to_u256s(g2_pt); + let neg_s_g2 = g2_to_u256s(neg_s_g2_pt); diff --git a/proofs/solidity-verifier/docs/audit/patches/applied-templates.patch b/proofs/solidity-verifier/docs/audit/patches/applied-templates.patch new file mode 100644 index 000000000..60ca65b82 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/patches/applied-templates.patch @@ -0,0 +1,230 @@ +--- a/templates/contracts/Halo2Verifier.sol ++++ b/templates/contracts/Halo2Verifier.sol +@@ -1,5 +1,16 @@ + // SPDX-License-Identifier: CC0-1.0 +-pragma solidity ^0.8.24; ++// Pinned, not floating. Two properties of this artifact are compiler- and ++// optimiser-dependent, and neither is visible in the source: ++// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR ++// upward. That is only safe while solc's stack-spill reservation stays ++// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not ++// a constant this file controls. verifyProof now asserts the separation. ++// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 ++// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over ++// the EIP-170 24,576-byte limit, so neither can be deployed. Only the ++// pinned (version, runs) pair is known to produce a deployable contract. ++// A floating `^0.8.24` advertises compatibility this contract does not have. ++pragma solidity 0.8.30; + + /// @title Halo2 BLS12-381 KZG verifier. + /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +@@ -51,12 +62,14 @@ + /// precompiles, or mismatched pinned dependency code revert. Trace and gas + /// renders keep the same failure policy. + /// @dev The generated verifier uses absolute Yul memory addresses instead +- /// of Solidity's free-memory pointer, but generated scratch starts at +- /// `0x80` so Solidity's reserved memory prefix is preserved. The main ++ /// of Solidity's free-memory pointer. Generated scratch starts at ++ /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's ++ /// stack-spill reservation below it untouched; the assembly block asserts ++ /// that separation on entry rather than assuming it. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the +- /// memory strategy; see `docs/MEMORY_LAYOUT.md`. ++ /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. +@@ -93,10 +106,20 @@ + {%- when None %} + {%- endmatch %} + assembly ("memory-safe") { ++ // The `memory-safe` annotation above is what enables solc's ++ // stack-to-memory mover, which reserves spill slots upward from ++ // 0x80. The generated layout below writes absolute addresses from ++ // TRANSCRIPT_MPTR upward and never consults the free-memory ++ // pointer, so the two regions must not meet. The size of that ++ // reservation is compiler-version and optimiser dependent, so ++ // assert the invariant in the deployed bytecode instead of relying ++ // on a generator-side test the integrator never runs. ~6 gas. ++ if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } ++ + // This block owns the call-frame memory and remains terminal. +- // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving ++ // Generated scratch starts at TRANSCRIPT_MPTR, preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot +- // words. See docs/MEMORY_LAYOUT.md. ++ // words. See docs/architecture/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== +--- a/templates/contracts/Halo2VerifyingKey.sol ++++ b/templates/contracts/Halo2VerifyingKey.sol +@@ -1,6 +1,9 @@ + // SPDX-License-Identifier: CC0-1.0 + +-pragma solidity ^0.8.24; ++// Pinned to match the verifier, so both halves of a deployment are provably ++// built by one toolchain. (This contract's runtime is pure returned data, so ++// its codehash is compiler-independent -- the pin is for the pair, not for it.) ++pragma solidity 0.8.30; + + /// @title Halo2 BLS12-381 verifying-key payload. + /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +--- a/templates/contracts/Halo2QuotientEvaluator.sol ++++ b/templates/contracts/Halo2QuotientEvaluator.sol +@@ -1,5 +1,8 @@ + // SPDX-License-Identifier: CC0-1.0 +-pragma solidity ^0.8.24; ++// Pinned to match the verifier, so both halves of a deployment are provably ++// built by one toolchain. (This contract's runtime is pure returned data, so ++// its codehash is compiler-independent -- the pin is for the pair, not for it.) ++pragma solidity 0.8.30; + + /// @title Split Halo2 quotient numerator evaluator. + /// @notice Reconstructs the scalar side of the linearization query for a generated verifier. +--- a/templates/partials/verifier/PrecompileSmoke.sol ++++ b/templates/partials/verifier/PrecompileSmoke.sol +@@ -2,6 +2,11 @@ + /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { ++ // Same free-memory-pointer guard as verifyProof. This body runs in ++ // the *creation* frame, which the generator's memoryguard test does ++ // not inspect (it parses the runtime prologue only). ++ if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { revert(0, 0) } ++ + // Scratch is reused for every runtime-prerequisite probe. + let scratch := {{ memory.constructor_smoke_scratch_mptr|hex() }} + +@@ -55,6 +60,90 @@ + ) + )) { revert(0, 0) } + ++ ++ // ---------------------------------------------------------------- ++ // Known-answer probes for the two precompiles that actually decide ++ // acceptance. ++ // ++ // Every probe above this point uses the point at infinity or a ++ // G1ADD vector. That leaves the two precompiles the verifier's ++ // security actually rests on untested for *rejection* behaviour: ++ // - 0x0c G1MSM is the curve/subgroup validator for every absorbed ++ // proof commitment (common_uncompressed_g1 runs no curve check); ++ // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose ++ // 0x0f always returns 1 accepts every proof. ++ // These four probes cost deployment gas only. ++ // ---------------------------------------------------------------- ++ ++ // (a) G1MSM known answer: [2]*G == 2G. ++ mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) ++ mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) ++ mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) ++ mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) ++ mstore(add(scratch, 0x80), 2) ++ if iszero(staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, 0xa0, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } ++ if iszero(and( ++ and( ++ eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), ++ eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) ++ ), ++ and( ++ eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), ++ eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) ++ ) ++ )) { revert(0, 0) } ++ ++ // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp ++ // but is NOT in the r-order subgroup (checked off-chain: r*P != O). ++ // EIP-2537 requires G1MSM to reject it. This is the one property ++ // the verifier's deferred-validation strategy depends on and the ++ // one property no other probe exercises. ++ // ++ // Gas is bounded on purpose: a precompile that rejects its input ++ // consumes everything forwarded to it, so an unbounded `gas()` here ++ // would burn 63/64 of the deployment gas before the probes below. ++ mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) ++ mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) ++ mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) ++ mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) ++ mstore(add(scratch, 0x80), 1) ++ if staticcall(200000, {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, 0xa0, scratch, {{ template_constants.g1_bytes|hex() }}) { revert(0, 0) } ++ ++ // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: ++ // with G1' = -G the product is 1, with G1' = +G it is not. G2 is ++ // written literally because the VK payload is not loaded during ++ // construction. ++ mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) ++ mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) ++ mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) ++ mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) ++ mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) ++ mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) ++ mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) ++ mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) ++ mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) ++ mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) ++ mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) ++ mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) ++ mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) ++ mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) ++ mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) ++ mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) ++ mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) ++ ++ // (c) e(G, G2) * e(-G, G2) == 1. ++ if iszero(staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, add(scratch, 0x300), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } ++ ++ // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. ++ mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) ++ mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) ++ if iszero(staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, add(scratch, 0x300), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } ++ if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } ++ if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } ++ + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { + mstore(add(scratch, off), 0) +--- a/scripts/install_pinned_solc.sh ++++ b/scripts/install_pinned_solc.sh +@@ -26,10 +26,36 @@ + mkdir -p "$INSTALL_DIR" + solc_path="$INSTALL_DIR/solc" + ++# Content hash, not version string. `--version` output is trivially forged by a ++# substituted binary, and this project's entire reproducibility claim rests on ++# the compiler being exactly this one. Per-platform hashes are published in ++# https://binaries.soliditylang.org/${platform}/list.json -- fill these in and ++# record them in the artifact manifest alongside the flag set. ++declare -A PINNED_SOLC_SHA256=( ++ ["linux-amd64"]="TODO-fill-from-list.json" ++ ["macosx-amd64"]="TODO-fill-from-list.json" ++) ++ + if [[ ! -x "$solc_path" ]] || ! "$solc_path" --version | grep -q "Version: ${PINNED_SOLC_VERSION}"; then + url="https://binaries.soliditylang.org/${platform}/${binary}" + echo "[install-solc] downloading $url" + curl -fsSL "$url" -o "$solc_path" ++ expected="${PINNED_SOLC_SHA256[$platform]:-}" ++ if [[ -z "$expected" || "$expected" == TODO-* ]]; then ++ echo "[install-solc] no pinned SHA-256 recorded for platform '$platform'." >&2 ++ echo "[install-solc] Fetch it from https://binaries.soliditylang.org/${platform}/list.json" >&2 ++ echo "[install-solc] and set PINNED_SOLC_SHA256 in this script." >&2 ++ rm -f "$solc_path" ++ exit 1 ++ fi ++ actual="$(shasum -a 256 "$solc_path" 2>/dev/null | cut -d' ' -f1 || sha256sum "$solc_path" | cut -d' ' -f1)" ++ if [[ "$actual" != "$expected" ]]; then ++ echo "[install-solc] SHA-256 mismatch for $url" >&2 ++ echo "[install-solc] expected $expected" >&2 ++ echo "[install-solc] actual $actual" >&2 ++ rm -f "$solc_path" ++ exit 1 ++ fi + chmod +x "$solc_path" + fi + diff --git a/proofs/solidity-verifier/docs/audit/patches/review-proposed-all.patch b/proofs/solidity-verifier/docs/audit/patches/review-proposed-all.patch new file mode 100644 index 000000000..b54093b53 --- /dev/null +++ b/proofs/solidity-verifier/docs/audit/patches/review-proposed-all.patch @@ -0,0 +1,177 @@ +--- a/templates/contracts/Halo2Verifier.sol 2026-08-12 15:35:48.921338927 +0000 ++++ b/templates/contracts/Halo2Verifier.sol 2026-08-12 15:36:19.199260815 +0000 +@@ -1,5 +1,19 @@ + // SPDX-License-Identifier: CC0-1.0 +-pragma solidity ^0.8.24; ++// Pinned, not floating. Two properties of this artifact are compiler- and ++// optimiser-dependent, and neither is self-evident from the source: ++// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR ++// upward, which is only safe while solc's stack-spill reservation stays ++// below it (observed 0x8c0 on 0.8.24, 0x8e0 on 0.8.26+). ++// 2. The runtime size depends on --optimize-runs. Measured: solc 0.8.24 at ++// runs=1 emits a 29,567-byte runtime and solc 0.8.30 at runs=100000 emits ++// 29,836 -- both exceed the EIP-170 24,576-byte limit and cannot be ++// deployed. Only the pinned (version, runs) pair is known to work. ++// A floating `^0.8.24` therefore advertises compatibility this contract does ++// not have. ++pragma solidity {{ pinned_solc_version }}; ++// Built with: solc {{ pinned_solc_version }} --via-ir --optimize ++// --optimize-runs {{ solc_optimize_runs }} --evm-version {{ solc_evm_version }} ++// --no-cbor-metadata + + /// @title Halo2 BLS12-381 KZG verifier. + /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +@@ -51,12 +65,14 @@ + /// precompiles, or mismatched pinned dependency code revert. Trace and gas + /// renders keep the same failure policy. + /// @dev The generated verifier uses absolute Yul memory addresses instead +- /// of Solidity's free-memory pointer, but generated scratch starts at +- /// `0x80` so Solidity's reserved memory prefix is preserved. The main ++ /// of Solidity's free-memory pointer. Generated scratch starts at ++ /// `TRANSCRIPT_MPTR` ({{ memory.transcript_mptr|hex() }}), leaving ++ /// Solidity's reserved prefix AND solc's stack-spill reservation below it ++ /// untouched; the assembly block asserts that separation at entry. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the +- /// memory strategy; see `docs/MEMORY_LAYOUT.md`. ++ /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. +@@ -93,10 +109,21 @@ + {%- when None %} + {%- endmatch %} + assembly ("memory-safe") { ++ // The `memory-safe` annotation above is what enables solc's ++ // stack-to-memory mover, which reserves spill slots upward from ++ // 0x80. The generated layout below writes absolute addresses from ++ // TRANSCRIPT_MPTR upward and never consults the free-memory ++ // pointer, so the two regions must not meet. That reservation is ++ // compiler-version and optimiser dependent -- it is not a constant ++ // this file controls -- so assert the separation here rather than ++ // relying on a generator-side test the integrator does not run. ++ // ~6 gas, once per proof. ++ if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } ++ + // This block owns the call-frame memory and remains terminal. +- // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving ++ // Generated scratch starts at TRANSCRIPT_MPTR, preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot +- // words. See docs/MEMORY_LAYOUT.md. ++ // words. See docs/architecture/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== +--- a/templates/contracts/Halo2VerifyingKey.sol 2026-08-12 15:35:48.930600580 +0000 ++++ b/templates/contracts/Halo2VerifyingKey.sol 2026-08-12 15:36:19.199534819 +0000 +@@ -1,6 +1,9 @@ + // SPDX-License-Identifier: CC0-1.0 + +-pragma solidity ^0.8.24; ++// Pinned to match the verifier. This contract's runtime is pure returned data, ++// so its codehash is compiler-independent -- but the pin keeps the two halves ++// of the deployment provably built by one toolchain. ++pragma solidity {{ pinned_solc_version }}; + + /// @title Halo2 BLS12-381 verifying-key payload. + /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +--- a/src/lowering/vk.rs 2026-08-12 15:35:48.939496697 +0000 ++++ b/src/lowering/vk.rs 2026-08-12 15:36:41.016340556 +0000 +@@ -93,6 +93,60 @@ + let g1_pt: G1Affine = G1Affine::generator(); + let g2_pt: G2Affine = self.params.g2().to_affine(); + let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); ++ ++ // The G1 base is validated above by reconstructing it from ++ // `g_lagrange`. Do the same work on the G2 side. Until now ++ // `g2` and `s_g2` were taken on trust from `params`, and ++ // `NEG_S_G2_BASE` is the element every soundness guarantee in the ++ // deployed verifier rests on: the final pairing is ++ // e(final_com - v*G + x3*pi, G2_BASE) * e(pi, NEG_S_G2_BASE) == 1 ++ // and anyone who knows the tau behind NEG_S_G2_BASE can forge an ++ // opening for any statement. ++ // ++ // Every other control in this repository -- quotient ++ // certification, the dual build, generator invariants, the VK ++ // codehash pin, the trace differential, the replay fixtures -- ++ // checks SELF-CONSISTENCY. An SRS substituted at build time ++ // produces a perfectly self-consistent artifact, so all of them ++ // pass. This assertion is the only place that can catch it. ++ ++ // 1. G2_BASE must be the canonical generator, for the same reason ++ // G1_BASE must be: the emitted equation assumes it. ++ assert_eq!( ++ g2_pt, ++ G2Affine::generator(), ++ "SRS G2 base is not the canonical BLS12-381 G2 generator; the \ ++ emitted pairing equation would not be the KZG identity" ++ ); ++ ++ // 2. `s_g2` must correspond to the SAME tau that produced ++ // `g_lagrange`. Commit f(X) = X in the Lagrange basis to obtain ++ // [tau]G1 = sum_i omega^i * L_i, then check the pairing ++ // e([tau]G1, G2) == e(G1, s_g2). This ties the G2 side of the ++ // key to the commitment basis the prover actually uses. ++ let omega = self.vk.get_domain().get_omega(); ++ let mut w = ::ONE; ++ let tau_g1 = g_lagrange ++ .iter() ++ .map(|g| { ++ let term = *g * w; ++ w *= omega; ++ term ++ }) ++ .fold(G1Projective::identity(), |acc, t| acc + t) ++ .to_affine(); ++ assert!( ++ srs_tau_is_consistent(&tau_g1, &self.params.s_g2().to_affine()), ++ "SRS inconsistency: s_g2 does not correspond to the tau that \ ++ generated g_lagrange. NEG_S_G2_BASE would be emitted from an \ ++ SRS unrelated to the commitment basis -- i.e. from a key whose \ ++ toxic waste may be known to whoever supplied it." ++ ); ++ ++ // TODO(deployment): additionally pin the SRS asset by SHA-256 here ++ // and record it, plus the ceremony transcript reference, in the ++ // artifact manifest. CODEGEN_ASSURANCE_DOSSIER.md already lists ++ // this as a required record; nothing currently produces it. + let g1 = g1_to_u256s(g1_pt); + let g2 = g2_to_u256s(g2_pt); + let neg_s_g2 = g2_to_u256s(neg_s_g2_pt); +--- a/scripts/install_pinned_solc.sh 2026-08-12 15:35:48.947553053 +0000 ++++ b/scripts/install_pinned_solc.sh 2026-08-12 15:36:19.199639949 +0000 +@@ -26,10 +26,33 @@ + mkdir -p "$INSTALL_DIR" + solc_path="$INSTALL_DIR/solc" + ++# Content hash, not version string. `--version` output is trivially forged by a ++# substituted binary, and this project's entire reproducibility claim rests on ++# the compiler being exactly this one. Hashes are published per platform in ++# https://binaries.soliditylang.org/${platform}/list.json. ++declare -A PINNED_SOLC_SHA256=( ++ ["linux-amd64"]="" ++ ["macosx-amd64"]="" ++) ++ + if [[ ! -x "$solc_path" ]] || ! "$solc_path" --version | grep -q "Version: ${PINNED_SOLC_VERSION}"; then + url="https://binaries.soliditylang.org/${platform}/${binary}" + echo "[install-solc] downloading $url" + curl -fsSL "$url" -o "$solc_path" ++ expected="${PINNED_SOLC_SHA256[$platform]:-}" ++ if [[ -z "$expected" || "$expected" == "" ]]; then ++ echo "[install-solc] no pinned SHA-256 recorded for platform '$platform'" >&2 ++ rm -f "$solc_path" ++ exit 1 ++ fi ++ actual="$(sha256sum "$solc_path" | cut -d' ' -f1)" ++ if [[ "$actual" != "$expected" ]]; then ++ echo "[install-solc] SHA-256 mismatch for $url" >&2 ++ echo "[install-solc] expected $expected" >&2 ++ echo "[install-solc] actual $actual" >&2 ++ rm -f "$solc_path" ++ exit 1 ++ fi + chmod +x "$solc_path" + fi + diff --git a/proofs/solidity-verifier/docs/benchmarks/BENCH.md b/proofs/solidity-verifier/docs/benchmarks/BENCH.md index d8fc67613..6ca7ca867 100644 --- a/proofs/solidity-verifier/docs/benchmarks/BENCH.md +++ b/proofs/solidity-verifier/docs/benchmarks/BENCH.md @@ -407,7 +407,7 @@ let prod_inv := scalar_inv(prod) Saves 13 modexp calls (~13 × 1.4 kg = ~18 kg) at the cost of ~28 muls (~280 gas). Net ~17 kg. -**Files:** `src/codegen/pcs.rs` (the emitter that lays out the +**Files:** `src/lowering/kzg/mod.rs` (the emitter that lays out the 14 `let _ := scalar_inv(_)` lines in the PCS block). Probably a dedicated `batch_scalar_inv` helper in the Yul prelude. @@ -419,12 +419,12 @@ the per-input zero check for defence in depth (revert if any is zero). ### C. Pre-fold `mulmod(_, 1)` / `addmod(_, 0)` in the evaluator codegen **Projection: 5–15 kg saved** -The gate evaluator (`src/codegen/evaluator.rs`) emits `mulmod(x, 1, r)` +The gate evaluator (`src/lowering/quotient_numerator/yul_emit.rs`) emits `mulmod(x, 1, r)` and `addmod(x, 0, r)` whenever a multiplicative or additive identity appears in the constraint. With `runs=1` solc cannot constant-fold these. Add a pass at codegen time that drops them. -**Files:** `src/codegen/evaluator.rs` (the `evaluate` recursion that +**Files:** `src/lowering/quotient_numerator/yul_emit.rs` (the recursion that emits per-expression Yul lines). **Risk:** must distinguish "literally constant `1`" (drop) from @@ -441,9 +441,9 @@ The gate evaluator currently re-mloads `Y_MPTR`, `THETA_MPTR`, 0.75 kg). Cheap to fix at codegen time — emit a `let y := mload(Y_MPTR)` at the top of the quotient block and reference `y` in each step. -**Files:** `src/codegen.rs` (the `make_block` closure that emits each -identity's Horner step), and `src/codegen/evaluator.rs` (the part that -substitutes `Y_MPTR` → local `y`). +**Files:** `src/lowering/quotient_numerator/` (`yul_emit.rs` emits each +identity's Horner step; `vm/mod.rs` is the VM path), including the part that +substitutes `Y_MPTR` → local `y`. **Risk:** none, mechanical. @@ -455,8 +455,8 @@ each repeat a 4-line `mstore(0x180, mload(...))` chain to copy 4-word points. Cancun ships MCOPY (`0x5e`); replace each chain with one `mcopy(dst, src, 0x80)`. -**Files:** `templates/contracts/Halo2Verifier.sol` and the `pcs_computations` -emitter in `src/codegen/pcs.rs`. +**Files:** `templates/contracts/Halo2Verifier.sol` and the `computations` +emitter in `src/lowering/kzg/mod.rs`. **Risk:** none. EVM target is already Cancun. @@ -559,7 +559,8 @@ and bumps the on-chain transcript's domain-separator epoch. Not as cheap as it looks — cross-stack coordination. **Files:** `midfall/proofs/src/transcript/mod.rs`, -`src/transcript.rs`, `templates/contracts/Halo2Verifier.sol`. Probably a +`templates/partials/verifier/TranscriptProofParser.yul` (with offsets from +`src/lowering/abi/`), `templates/contracts/Halo2Verifier.sol`. Probably a follow-up after A is shipped. ## Realistic projection after Step 6 @@ -621,6 +622,6 @@ the floor is dominated by EIP-2537 pricing and the cryptographic work. section (modulo the 750-gas overhead per checkpoint, which is subtracted by `dump_gas_checkpoints`). - For tighter attribution within the 631 kg PCS block, add additional - checkpoints inside `src/codegen/pcs.rs::computations()` at the + checkpoints inside `src/lowering/kzg/mod.rs::computations()` at the per-set boundaries. Currently every set's three sub-stages (point set group, batch invert, MSM) coalesce into the same 631 kg bucket. diff --git a/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md b/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md index 5f79c6c78..83c2c2eae 100644 --- a/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md +++ b/proofs/solidity-verifier/docs/benchmarks/OPTIMISATION.md @@ -262,7 +262,9 @@ Files touched: - `vendor/.../midfall/proofs/src/transcript/implementors.rs` — `Hashable for G1Projective::to_input` returns 128 bytes instead of `::to_bytes` (compressed). -- `src/transcript.rs::common_g1` — absorbs the same 128 bytes; the +- `common_g1` (at the time in `src/transcript.rs`; the transcript helpers now + live in `templates/partials/verifier/TranscriptProofParser.yul` with offsets + in `src/lowering/abi/`) — absorbs the same 128 bytes; the `common_g1_then_squeeze_matches` round-trip test pins this to `CircuitTranscript`'s output. - `templates/contracts/Halo2Verifier.sol::common_uncompressed_g1` — replaced a diff --git a/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md b/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md index 8b1c23b75..8afd16ab2 100644 --- a/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md +++ b/proofs/solidity-verifier/docs/benchmarks/PCS_BLOCK5_FINAL_MSM_ANALYSIS.md @@ -74,7 +74,7 @@ let v = inner_product([q_evals..., f_eval], powers(x4)); The Solidity codegen for this is in: ```text -src/codegen/pcs.rs +src/lowering/kzg/mod.rs ``` around the generated `build final_com and v` block. diff --git a/proofs/solidity-verifier/docs/plans/REDESIGN_PROPOSALS_2026-08.md b/proofs/solidity-verifier/docs/plans/REDESIGN_PROPOSALS_2026-08.md new file mode 100644 index 000000000..d906283d7 --- /dev/null +++ b/proofs/solidity-verifier/docs/plans/REDESIGN_PROPOSALS_2026-08.md @@ -0,0 +1,500 @@ +# Robustness & Quality Assessment and Redesign Proposals + +> Companion to +> [`../architecture/ARCHITECTURE_REVIEW_2026-08.md`](../architecture/ARCHITECTURE_REVIEW_2026-08.md). +> Produced 2026-08-08 against the `misc-fixes` branch by an independent +> multi-reviewer pass: nine subsystem mapping reviews, four assessment lenses +> (soundness, library robustness, on-chain security, architecture quality), +> and a second adversarial pass in which every finding cited below was +> re-verified against the source by a reviewer instructed to refute it. +> Findings are labeled **Confirmed** (every element checked in code) or +> **Partial** (core claim holds; stated corrections apply). Line numbers are +> as of this snapshot. As with any review, verify accuracy and completeness +> against the current sources before acting on it. + +## 1. Overall verdict + +| Lens | Score | Summary | +| --- | :-: | --- | +| Soundness of generated verifiers | 8/10 | No accept-invalid path or calldata malleability found. Calldata parsing is injective and fully pinned; every scalar/coordinate ingress is canonicality-checked; subgroup checking is delegated under a machine-checked coverage invariant; the quotient-VM certification chain is genuine defense-in-depth. Residual risk sits in what is assumed rather than proven (§3.2) and in assurance-process gaps (F1). | +| Robustness as a library | 7/10 | Two well-executed tiers — typed errors at the boundary, fail-closed asserts inside — that contradict each other's contracts: legal-but-unusual circuits pass `try_new` and then panic inside `render()`/`encode_calldata()` (F2), and per-proof encoding rebuilds the whole pipeline (F4). | +| Security of generated on-chain code | 8/10 | Precompile discipline, pinning, and fail-closed parsing are unusually strong. Must-fix hardening: the false `memory-safe` annotation + floating pragma with no runtime guard (F3), and dead unpinned-quotient template branches (F13). | +| Architecture quality & maintainability | 7/10 | Clean, verified macro-architecture (facade → snapshot → converged plan → declarative render). Liabilities are consistent in kind: strings and conventions doing work types should do (F7, F8, F10, F11), two oversized modules (F14), and measurable docs rot in the audit chain (F6, F15). | + +The failure direction is consistently **crash-or-reject** — the right default +for this domain. Nothing found blocks the bounded correctness claim in +`docs/audit/CODEGEN_ASSURANCE_DOSSIER.md`; most of what follows lowers the +cost and raises the credibility of the external audit this repo is explicitly +preparing for. + +## 2. What is strong — and should not be redesigned + +Explicit non-goals for any redesign; these choices are earning their keep: + +- **The narrow supported envelope** and fail-fast `try_new` validation. +- **The static absolute memory layout** with the lifetime-aware overlap + validator. (Harden its seams — F5, F9 — do not replace it with FMP-based + allocation.) +- **The compact quotient VM in the codehash-pinned VK payload**, and the + render-time certification chain: 3-stage validators, pointer whitelist, + reference-interpreter certification at an artifact-seeded challenge, + dual-build (recognizers on/off) agreement, and word-for-word re-comparison + of VK-embedded program bytes. Keep `vm/reference.rs` deliberately + independent of the emitter — the N-version leg is the point. +- **Single converged `LoweringPlan` per `render()` call** (extend it with + caching, F4 — don't weaken it). +- **Pinning by runtime length + codehash, re-checked per call.** +- **Templates-consume-facts** and `QUOTIENT_VM_SPEC` as the single VM ABI + source. +- **Loud-failure test prerequisites, gas-based stage attribution in replay + tests, and self-describing fixtures.** + +## 3. Findings register + +### 3.1 High severity (all Confirmed) + +**F1 — The adversarial EVM suite is excluded from every CI workflow by the +`pbt_` name filter; the CI comment claims otherwise.** +`.github/workflows/ci.yaml` (EVM job) runs +`cargo test -p halo2_solidity_verifier --release --all-features pbt_` plus the +Poseidon fixture; the default job leaves `HALO2_SOLIDITY_RUN_EVM_TESTS` unset +so EVM tests self-skip; the bench workflow runs only the IVC bench scripts. +Tests that therefore run in **no** workflow include the strongest +security-relevant negatives: `every_proof_g1_rejects_noncanonical_coordinates` +(`src/test.rs:2868`), `..._base_modulus_coordinates` (:2914), +`..._off_curve_coordinates` (:2948), +`compiled_memoryguard_does_not_overlap_generated_layout` (:1441), +`compiled_verifier_runtime_fits_the_eip170_limit` (:1495), +`batch_invert_fails_closed_on_noncanonical_words_in_all_paths` (:1584), +`malformed_embedded_calldata_variants_are_rejected` (:1284), +`vk_payload_section_mutations_are_rejected` (:1349), +`supported_shape_circuit_fuzz_e2e` (:501), and +`same_srs_distinct_shape_matrix_rejects_cross_wiring` (:629). The CI step +comment explicitly claims "non-canonical scalar and G1 rejection, EIP-170 +runtime size, and the memoryguard overlap check" run in that step — they do +not. A regression in any of these merges green. → Proposal P0.1. + +**F2 — Extensive panic surface behind the Result-typed public API.** +`render()`, `render_quotient_evaluator()`, `repack_proof()`, +`encode_calldata()`, and both diagnostics route through `LoweringPlan::new`, +which converts every post-constructor failure into a panic +(`src/lowering/plan.rs:167,183`; convergence panics at +`src/lowering/vk.rs:231-234,367`; model validation at +`src/lowering/artifacts.rs:66,296`; plan validation `.expect` at +`src/lowering/quotient.rs:469`). Legal shapes that pass `try_new` and then +panic include: >28 distinct PCS rotation points, rotation spans >4096, +circuits whose VK payload exceeds EIP-170, and VKs with unremoved virtual +selectors — while `try_new`'s docs promise "a typed error when the supplied +constraint system is outside the currently supported shape" and +`GeneratorConfig`'s docs claim the constructor validates the entire shape up +front. `vm/reference.rs:129-130` even documents that a malformed stream "must +surface as a `GeneratorError`, not a process abort," yet its only production +call site converts the `Result` to a panic. → Proposal P1.1. + +**F3 — False `("memory-safe")` annotation + floating `pragma ^0.8.24`, with +no per-PR or on-chain enforcement of spill-window disjointness.** +The terminal assembly block writes absolute addresses from 0x1000 up and +never allocates via the free-memory pointer; the annotation is documented +in-repo as "a false promise" that is load-bearing (the block does not compile +without it). Disjointness of solc's via-IR spill reservation (observed up to +0x8e0 of the 0xF80 headroom) from the generated layout is checked only by +`compiled_memoryguard_does_not_overlap_generated_layout` — env-gated and, per +F1, not run in CI. The floating pragma permits deployers to compile with any +0.8.x ≥ 24, unbinding the artifact from the pinned compiler the check was +measured against. Nothing at deploy or run time detects an overlap; a live +spill slot sharing bytes with the transcript buffer would corrupt verifier +state silently. → Proposal P0.2. + +**F4 — Every per-proof `repack_proof`/`encode_calldata` call rebuilds the +entire lowering pipeline, including an O(2^k) SRS fold and two certification +passes.** `src/lowering/calldata.rs:157-159` calls +`self.lowering_plan().repacked_proof_layout_plan()` per proof; +`LoweringPlan::new` runs `generate_vk` — whose `generate_base_vk` folds every +`g_lagrange` point to assert the SRS base (`src/lowering/vk.rs:81-92`; k=20 +for the IVC shape) — plus both convergence loops, full VM compilation, and +both certifications. `render()`, `proof_evaluation_counts()`, and +`quotient_identity_manifest()` each independently rebuild the same plan; +nothing on `SolidityGenerator` caches it, and the doc comment states each +path builds its own plan. Cross-call consistency rests on determinism by +convention (pinned by one repeated-plan test). This is seconds of latency and +a realistic DoS surface for a service encoding many proofs. → Proposal P1.2. + +**F5 — Cross-phase memory-overlap soundness hangs on `MemoryPhase` +declaration order matching template include order, enforced only by a +comment.** `MemoryLifetime::intersects` treats distinct `Phase` lifetimes as +never co-live and compares `PhaseSpan`s with the derived `Ord` +(`src/lowering/layout/memory.rs:192`); the sync requirement with +`Halo2Verifier.sol` include order exists only as a doc comment +(memory.rs:121-127). A mis-ordered variant silently disables overlap +detection for the affected pair. The model's acknowledged blind spot — the +0x1000-band `AccumulatorPairingBatch`/`FinalPairing` frames — is asserted +disjoint only under `cfg(test)` (memory.rs:1522-1539), not in +`VerifierMemoryLayout::validate()`. (Correction from verification: that +specific pair is currently disjoint *by construction* — +`FINAL_PAIRING_SCRATCH_START = PAIRING_BATCH_PTR + PAIRING_BATCH_HASH_BYTES` +— so the exposure is to future edits, not the current layout.) → Proposal +P0.3 / P2.2. + +**F6 — `REPRODUCIBLE_BUILDS.md`'s dependency-pinning claim contradicts +`Cargo.toml`.** The doc claims "All Midfall crates are resolved from the +pinned git revision in Cargo.toml"; `Cargo.toml` declares only workspace path +dependencies (`midnight-proofs = { path = ".." }`, etc.), so published +runtime hashes are a function of the enclosing workspace checkout. Mitigating +nuance: the doc records the Midfall revision in prose, so a careful reader +can still reproduce — but the stated mechanism is false in the document that +underwrites the hashes. → Proposal P0.4. + +### 3.2 Residual assurance boundary (documented, not a defect) + +The certification chain proves the **emitter → reference-interpreter** leg +per render. The **reference-interpreter → Yul** leg rests on opcode-table +conformance tests (a `case` exists per opcode — not that the case body is +correct) plus native/Solidity trace differentials on fixture circuits; an +opcode no fixture emits has unverified runtime semantics. Identities executed +as inline Yul, native callbacks, or the structured tail are never lowered to +bytecode and are covered only by the trace differentials. This is honestly +documented (`LOWERING_ARCHITECTURE_SPEC.md` §12.1, `vm/reference.rs:20-27`) +— listed here because Proposal P2.5 can close most of it, and because the +trace differential that covers it is itself env-gated (F1). + +### 3.3 Medium severity + +**F7 — Yul text is an internal IR** *(Partial — corrected)*. The evaluator +emits Yul strings; permutation/lookup/trash identities are re-parsed from +those strings into `QuotientExpr` trees by a shallow parser +(`src/lowering/quotient.rs:939-945` → `vm/mod.rs:1236-1279`); limb7 fusion is +textual pattern-matching (`quotient.rs:1172-1311`); helper inclusion is +substring scanning (`quotient.rs:93-118`). Correction from verification: in +actual builds the parsed families become native callbacks/structured tail, +which `certify_quotient_program` **skips** (certify.rs:176-180) — interpreted +gate identities are certified against trees from typed `Expression` +lowering, not the parse. Net effect stands: no check compares the parsed +trees against an independent source, so a parser bug on those families is +invisible to certification and caught only by the env-gated EVM trace +differential. → Proposal P2.6. + +**F8 — MODARITH7/AFFINE_SUM operand layouts hand-decoded in five Rust sites +plus the template** *(Confirmed, sites enumerated)*: +`validate_quotient_const_slots` (vm/mod.rs:2134-2192), the pointer walker +`quotient_read_pointers` (:2424-2506), the checked length walkers +(:2578-2680), the separate trusted length walkers (:3073-3105), the reference +interpreter (reference.rs:280-376, 436+), and +`QuotientNumeratorBlock.yul:715-757, 896-1052`. Certification cannot catch a +validator walker diverging from the emitter/reference pair. → Proposal P2.1. + +**F9 — Panicking "trusted" bytecode walkers are safe only under a +validate-first convention** *(Confirmed)*. `quotient_op_len` panics on +unknown opcodes and reads without bounds checks; the run-after-validation +convention lives in doc comments only, and one call site +(`compact_quotient_runs`, vm/mod.rs:1931-1983) walks builder-emitted bytes +*before* validation runs in `finish()`. → Proposal P2.1. + +**F10 — Injected code blocks couple to template scope by naming convention** +*(Confirmed)*. `Vec>` Yul lines (models.rs:519-526, 635-640) are +spliced verbatim and reference template locals (`r`, `y`, `delta`, +`quotient_eval_numer`, …) with no identifier validation; only solc would +catch a mismatch. Related *(Partial)*: models validate at construction inside +`generate_*_from_plan` (panicking), but `render()` itself does not enforce +`validate_layout()` — a future direct-construction call site could render an +unvalidated model. → Proposals P1.3, P2.6. + +**F11 — Template security behavior pinned by raw-text `contains()` +assertions** *(Partial — incident nuance)*. Guard tests assert substrings of +the **unrendered** template corpus (`src/lowering/tests.rs:1624-1690`). +Verification surfaced a concrete prior incident: commit `72bc1b2` +(2026-07-19) fixed an always-false Yul guard in `AccumulatorHelpers.yul` +that left `load_acc_point`'s `if is_id` branch dead while the adjacent +`contains()` test stayed green (the decoder remained fail-closed; no forgery +was possible — the incident shows the test style's blindness, not a +vulnerability). → Proposal P2.5. + +**F12 — Feature profile recorded nowhere machine-readable in artifacts** +*(Partial)*. `truncated-challenges`, `outer-fewer-point-sets`, and +`outer-single-h-commitment` change the proof schema the rendered verifier +expects; a mismatch fails as bare `revert(0,0)` (hardcoded proof-length check +or failed pairing) with no diagnostic. Correction: truncated-challenges is +human-discernible via a rendered comment; the other two leave no named trace. +Also *(Confirmed)*: `RenderDiagnostics::default()` is intentionally +feature-gated, so default render shape depends on compile features. +→ Proposal P1.4. + +**F13 — Dead unpinned external-quotient template branches** *(Partial — +guards stronger than claimed)*. `Constructors.sol:63-82` (and the two-arg +variant) render a constructor without a codehash `require` when +`expected_quotient_codehash` is `None`, and `QuotientAndLinearization.yul` +omits the pre-call codehash re-check in the same case. Corrections: the +configuration is unrepresentable through the public API (`RenderQuotient` has +only `Inline` and `ExternalPinned`), and the "deprecated panicking APIs" +mentioned in the spec no longer exist in source — the guards are the enum +shape plus an `artifacts.rs` assert. The hole-shaped template text is still +dead weight that an auditor must reason away. → Proposal P1.3. + +**F14 — Module hygiene: two monoliths** *(Confirmed)*. `vm/mod.rs` is 4,382 +lines mixing the opcode ABI, builder, three validators, shape recognizers, +the Yul parser, and proof-repack layout types that belong to the calldata +boundary (`RepackedProofLayoutPlan`). `kzg::computations` is one 1,014-line +function (kzg/mod.rs:834-1847) whose trace-only q_com walk (:1226-1334) +structurally duplicates Block 5's final-MSM term enumeration (:1673-1731). +→ Proposal P2.3. + +**F15 — Audit-chain docs rot** *(Confirmed)*. +`CODEGEN_ASSURANCE_DOSSIER.md:59-64`, `AUDIT.md`, and `AUDIT_FINDINGS.md` +cite the vanished pre-rename `codegen` tree, since renamed to +`src/lowering/` (REVIEW_PACKET.md is current — the +audit docs disagree with each other); README carries two stale, +self-contradictory test counts (167+4 at line 49, 177 at line 389; actual at +snapshot: 202 lib + 9 integration) and a broken `./TESTING_STRATEGY.md` link; +`ROADMAP.md` links a nonexistent `./AUDIT.md` and presents fixed blockers as +open; STATUS/dossier are a dated snapshot ("suite was not rerun") with no +re-assessment trigger. → Proposal P0.4. + +**F16 — Diagnostics re-derivation** *(Partial)*. Public +`quotient_identity_manifest` re-derives gates/targets from `vk.cs()` without +consulting the executed plan; the plan-derived manifest exists but is +`cfg(test)`-only (vm/mod.rs:258-282). Correction: the selector-detection +method is identical to the plan's, not divergent as originally claimed; +`ProofEvaluationCounts` hand-duplicates protocol formulas with a total-sum +assert as the only cross-check. → Proposal P1.5. + +**F17 — Test-hygiene trio** *(Confirmed)*. (a) The bit-flip malleation PBT +reaches only the first 512 proof bytes (`bit_idx ∈ 0..4096`, byte 511 max; +the repacked proof is several thousand bytes — the tail G1s/q_evals/π are +never flipped by this test; scalar-sweep tests do cover deeper offsets +structurally). (b) Nine adversarial tests pass vacuously (stderr notice only) +when the accept baseline breaks, via `solidity_output_is_true_or_skip` +(test.rs:3411-3429); a loud variant exists and is used by the malleation +baseline. (c) The PBT runner defaults to 3 cases with +`failure_persistence: None` (test.rs:4295-4306); the two libFuzzer targets +are wired to no workflow. → Proposal P0.1. + +### 3.4 Low severity (verified; batch as hygiene) + +- **L1** `scalar_inv` scratch registers 0xc0 of the historical 0x100 window; + the 0x40 gap is unused padding but unregistered/unvalidated + (memory.rs:700-705). +- **L2** `num_instances` has no sanity bound on the non-accumulator path (an + absurd value renders a verifier demanding `num_instances*32` bytes of + calldata); accumulator configs do bound it via the fixed-base tail check. +- **L3** `evm.rs` harness: pervasive documented panics, substring parsing of + solc's human-readable output (single-contract, trailing-newline + assumptions), publicly re-exported behind the `evm` feature; + `Evm::code_size` doc/impl mismatch. +- **L4** Several consistency checks are `debug_assert!`-only and compile out + in release (diagnostics.rs:18, calldata.rs:127, memory.rs:97-103/277, + encoding/mod.rs:881/970, protocol/mod.rs:676, kzg/mod.rs:1117-1119); + `Constants.sol:24` unwraps `expected_quotient_len` inside the + `expected_quotient_codehash` match arm — safe only via the artifacts.rs + pairing invariant. +- **L5** The constructor precompile smoke cannot detect a G1MSM + implementation that omits subgroup checks (its MSM probe is identity-only; + acknowledged in the file's own comments) — the exact property the + verifier's soundness delegates to the precompile. +- **L6** Stale NatSpec in the template and every rendered fixture/deployment + claims generated scratch starts at 0x80; the actual base is 0x1000. +- **L7** Trace renders switch the evaluator staticcall to CALL and drop + `view` with no structured non-production marker (buried comments only; + state change limited to LOG1 by the pinned callee). +- **L8** Small duplications/dead code reported by reviewers: `u256_string` + duplicated verbatim (yul_emit.rs:1175 / vm/mod.rs:4168), two pow5 + recognizers, dead `_quotient_max_stack` binding (quotient.rs:134), empty + `render/yul.rs`, dead `hex_padded` branch and duplicate `proof_len` check + in models.rs. (Note: the hardcoded 8/10 accumulator word constants in + `api.rs` originally reported here turned out to have a tying test — + `lowering/tests.rs:1692-1707` — and are dropped as a finding.) + +## 4. Redesign proposals + +Ordered by (leverage ÷ risk). P0 items are small and should land before the +audit; none change verifier semantics. Items marked ⚠ change rendered +bytecode and require regenerating pinned fixtures and the +`REPRODUCIBLE_BUILDS.md` hashes — batch them into one regeneration. + +### P0 — Assurance-process fixes (small, high leverage) + +**P0.1 Fix CI test selection and skip semantics** *(F1, F17)*. +Enumerate the env-gated EVM tests explicitly in `ci.yaml` (or adopt one +enforced prefix and add a meta-test asserting the filter matches every +`#[test]` in `src/test.rs`); add a post-step check that the executed-test +count is non-zero and matches expectation so a filter regression fails +loudly. Replace `solidity_output_is_true_or_skip` with the existing loud +variant when `HALO2_SOLIDITY_RUN_EVM_TESTS=1` (matching the suite's own +loud-prerequisite philosophy). Draw `bit_idx` from `0..proof_len*8`. Wire the +two libFuzzer targets into a scheduled workflow with a committed corpus; give +the PBT runner a persistence file and a higher default in CI. Move the +slowest tests to the weekly workflow if per-PR time matters — but into *some* +workflow. Fix the misleading CI step comment. *Risk: CI minutes.* + +**P0.2 Runtime free-memory-pointer guard + pragma pin** *(F3)* ⚠. +Emit `if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) }` as the first +statement of the terminal assembly block in both `Halo2Verifier` and +`Halo2QuotientEvaluator` — via-IR initializes the FMP to the memoryguard +value, so this converts the spill-window assumption into a fail-closed +on-chain check for ~10 gas per proof. Pin the pragma to the audited compiler +(or document why not). Keep the memoryguard host test and run it per-PR +(P0.1). *Risk: minimal; confirm the legacy pipeline also initializes 0x40 +before the block (it does).* + +**P0.3 Promote by-construction frame checks into `validate()`** *(F5)*. +Move the `AccumulatorPairingBatch`/`FinalPairing` disjointness assertion from +`cfg(test)` into `VerifierMemoryLayout::validate()` so every generation is +gated on it. Cheap, generation-time only. + +**P0.4 One documentation-refresh pass with drift guards** *(F6, F15, L6)*. +Correct the `REPRODUCIBLE_BUILDS.md` pinning claim and record the workspace +commit alongside published hashes (the bench script can emit it into +`contract-sizes.txt`); update stale pre-rename `codegen` paths (now +`src/lowering/`) in the dossier and audit +docs; replace README test counts with the command that produces them; fix +broken links; add status markers to ROADMAP; fix the 0x80 NatSpec ⚠; define a +re-assessment trigger (any change under `src/lowering` or `templates/` bumps +a dated stamp). Guard recurrence with a CI link checker plus a small test +asserting every path cited in `docs/audit` exists. *No code risk; high +audit-readiness leverage.* + +### P1 — API contract and performance (small–medium) + +**P1.1 Make the lowering pipeline fallible end-to-end** *(F2, F9-adjacent)*. +The leaf validators already return `Result<_, String>`: change +`LoweringPlan::new` to `try_new -> Result`, +threading through `generate_vk`, `meta_data_for_stable_static_layout`, +model validation, and certification. Introduce an internal error taxonomy +{UnsupportedShape, ResourceLimit, InternalInvariant} carried in +`GeneratorError::Planning` so callers can distinguish rejection classes. +Keep panics only for true internal-invariant bugs, and document them under +`# Panics` on every public method that retains one. Honor the +`reference.rs` contract that certification failures surface as +`GeneratorError`. *Risk: low functionally; moderate churn (~8 modules); keep +the current panic messages as error text.* + +**P1.2 Cache one converged plan per generator; decouple repacking** *(F4)*. +Store `OnceCell, GeneratorError>>` on +`SolidityGenerator` (inputs are already immutable; determinism is pinned by +the repeated-plan test) and route every entry point through it — this also +*structurally* guarantees render/repack/diagnostics agree on one plan instead +of by convention. Additionally derive `RepackedProofLayoutPlan` directly from +`ProofCalldataLayout::from_protocol` + meta counts so per-proof calldata +encoding never needs VK generation or certification at all. *Risk: low.* + +**P1.3 Make invalid render states unrepresentable** *(F10, F13, L4)* ⚠. +Collapse `quotient_external` + `expected_quotient_{len,codehash}` into one +`Option` model field; delete the +`when None` codehash-free branches from `Constructors.sol` and +`QuotientAndLinearization.yul` (already unreachable; removing them deletes +the hole-shaped text and the template-level `unwrap()`). Seal model +construction behind validating constructors (or have `render()` call +`validate_layout()` first) so an unvalidated model cannot render. *Risk: +none functionally; fixture regeneration.* + +**P1.4 Record the feature/build profile in rendered artifacts** *(F12)* ⚠. +Emit a feature-profile constant (bitmask or three booleans + crate version) +and a NatSpec line in `Halo2Verifier.sol`; expose it as a typed field on +`RenderedArtifacts`; include it in the reproducible-builds manifest; surface +it in `RepackError` context so an off-chain repack against the wrong profile +is diagnosable in one step. Consider making `RenderDiagnostics::default()` +feature-independent (breaking change to a documented-intentional behavior — +decide explicitly). Add a CI matrix job compiling + running lib tests under +default features and each outer-* feature alone. *Risk: pinned-hash +regeneration.* + +**P1.5 Plan-derived diagnostics only** *(F16)*. +Remove the `cfg(test)` gate on the plan-derived manifest and implement +`quotient_identity_manifest` as `plan.quotient_identity_parts().manifest()` +via the cached plan; derive `ProofEvaluationCounts` from the protocol plan's +per-family counts, keeping the `meta.num_evals` assert as the net. *Risk: +low; output shape already pinned by tests.* + +### P2 — Structural improvements (medium–large) + +**P2.1 Single operand-layout descriptor + `ValidatedProgram` newtype** +*(F8, F9)*. Define each opcode's operand schema once (typed field sequence: +u8 const-slot, u16 ptr, counted blocks) attached to `QUOTIENT_VM_SPEC`, and +drive the checked length walker, const-slot validator, pointer walker, and +trusted walker from it; render the schema into the template's case +documentation so the Yul reviewer diffs against the same source. Keep +`reference.rs`'s decoder deliberately independent (document that choice — it +is the N-version defense). Add `ValidatedProgram<'a>` constructible only via +`validate_quotient_program`, and make the panicking walkers take it. *Risk: +touches the trusted emission path; land under the existing conformance tests ++ dual-build certification as the net.* + +**P2.2 Mechanize the `MemoryPhase` ↔ template-schedule contract** *(F5)*. +Introduce a schedule manifest — a const ordered list of (phase, template +partial, marker) — that (a) the enum order is asserted against and (b) a lib +test extracts from rendered verifier output (per-section markers already +exist in gas-checkpoint form; add a non-feature-gated comment marker) so a +reordered include or misplaced enum variant fails a test instead of silently +disabling overlap detection. Optionally add explicit sub-phase ordering for +the 0x1000 band so its aliasing becomes checkable rather than exempt. + +**P2.3 Split the monoliths; move repack types to the ABI boundary** *(F14)*. +Mechanical split of `vm/mod.rs` into `vm/{spec,builder,validate,recognize, +yul_parse}.rs`; move `RepackedProofLayoutPlan` (and friends) to +`lowering/abi/`. Split `kzg::computations` into per-block functions sharing +one linearization-term enumeration source, eliminating the trace/Block-5 +duplicate walk. *Risk: low; no output change; pure `pub(crate)` refactor.* + +**P2.4 Negative-conformance subgroup probe at deployment** *(L5)* ⚠. +Embed one constant on-curve, non-subgroup G1 point in the constructor smoke +and require the G1MSM staticcall on it to *fail* (optionally a non-subgroup +G2 for the pairing). This makes the deployment gate test the exact rejection +behavior the runtime soundness delegates to the precompile. Generate the +constants once with a Rust-side test proving on-curve ∧ outside r-torsion. +*Risk: slight deployment gas.* + +**P2.5 Render-time differential execution of the assembled quotient Yul** +*(§3.2, F7, F11)*. Behind the `evm` feature (or a release-render gate), +execute the rendered quotient block on revm against a memory image generated +from `QuotientRefMemory`'s address-derived assignment and compare `-nu_y(x)` +plus every selector bucket with the reference interpreter. This closes the +reference→Yul leg and covers inline/native/tail identities per artifact +instead of per fixture — converting the two weakest assurance legs into a +render gate. Complements, not replaces, the raw-text template tests (which +should migrate to rendered-output assertions where feasible). *Risk: needs +the EVM harness inside generation (feature-gated); the memory image must +respect the structural constraints native kernels assume — the read-model +windows already describe them.* + +**P2.6 Typed statement IR instead of the Yul-string round-trip** *(F7, F10)* +⚠ *(large; do last)*. Extend `QuotientExpr` into a small statement IR +(let-bindings + expression), make `yul_emit::Evaluator` produce it once per +identity, and derive everything from it: (a) one printer to Yul text for +inline/native blocks, (b) direct VM compilation (`emit_expr` already consumes +`QuotientExpr`), (c) limb7 fusion and pow5/helper detection as structural +rewrites/queries. Delete the assignment parser, the textual +`specialize_limb7_chains` matcher, one of the two pow5 recognizers, and the +substring helper-flag scan. This removes the largest class of convention +coupling and the certification blind spot at the parse seam. *Risk: medium — +output text will change, churning fixtures and published hashes; stage with +output-identity checks first, then the P2.5 differential as the semantic +net.* + +### P3 — Hygiene batch (small, opportunistic) + +Bound `num_instances` sanely at `try_new` (L2); promote the +security-adjacent `debug_assert!`s to `assert!` or `validate()` checks — +codegen is not hot enough to care (L4); register the full `scalar_inv` +0x100 window or update the stale comment (L1); harden `evm.rs` (solc +`--combined-json` output instead of substring parsing, fix the `code_size` +doc, deduplicate the two compile/two run-tx paths) (L3); add a structured +non-production marker to trace renders (contract-name suffix or NatSpec tag) +(L7) ⚠; delete the small duplications/dead code (L8). + +## 5. Suggested sequencing + +1. **Week 1 (audit-readiness):** P0.1–P0.4. Nothing here changes semantics; + P0.2 and the NatSpec fix change bytecode — do one coordinated fixture/hash + regeneration. +2. **Next:** P1.2 (caching — smallest high-value code change), then P1.1 + (Result threading) since its churn benefits from the single cached entry + point; P1.3–P1.5 ride the same fixture regeneration as P0.2. +3. **Then:** P2.1–P2.4 in any order (independent); P2.5 before P2.6 so the + IR migration lands under a per-render semantic differential. + +## 6. Verification note + +This assessment was produced by automated review with per-finding +adversarial re-verification against the source; corrections discovered +during that pass are recorded inline. Findings describe the snapshot above. +Review the accuracy and completeness of both documents against the current +tree before relying on them — responsibility for verification remains with +the reader. diff --git a/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md b/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md index 1754667bb..ff497eaed 100644 --- a/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md +++ b/proofs/solidity-verifier/docs/plans/SPLIT_NWAY_NOTES.md @@ -103,7 +103,7 @@ within the noise budget of solc's optimizer rearrangements. ## Files touched -- `src/codegen.rs` +- `src/lowering/mod.rs` (at the time the top-level `codegen` module) - `render_with_quotient_helpers()` -> thin wrapper over `render_with_quotient_helpers_n(2)`. - `render_with_quotient_helpers_n(N)` -> new public API. @@ -112,7 +112,7 @@ within the noise budget of solc's optimizer rearrangements. - split quotient helper rendering with per-identity inline-asm spill (replaces `_vS`/`_vL` external calls). - `collect_named_refs()` helper. -- `src/codegen/template.rs` +- `src/lowering/render/models.rs` (at the time the template model module) - `Halo2Verifier.quotient_helpers_n: usize` field. - `src/evm.rs` - `Evm::create_with_address_and_address_array_arg(...)` to deploy diff --git a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md index c5743ebd3..a1d6cddd3 100644 --- a/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md +++ b/proofs/solidity-verifier/docs/plans/TESTING_STRATEGY.md @@ -500,7 +500,7 @@ checkpoint. | PCS/KZG/quotient | Mutate every quotient commitment, proof eval, opening proof, batching scalar source, quotient evaluator output, and external quotient return length. Assert the final pairing result is semantically checked, not just precompile call success. | | Accumulator-specific | Check accumulator schema consumes exactly the expected public input words. Test unused high limb bits, malformed identity encoding, x/y limb swaps, scalar mutation, zero/identity accumulator cases, and any future fixed-base tail. | | Precompile/fail behavior | Constructor smoke tests cover MCOPY, the largest generated G1MSM input, and the two-pair EIP-2537 pairing shape; add tests for short return data, false pairing result, reverted precompile call, and stale return memory using generated-template mutations or a helper harness. | -| Memory/layout | Fast generator tests should assert no overlap between VK, challenge, transcript, quotient, PCS, accumulator, and scratch regions. Keep these as compile-time/layout tests in `src/codegen/mod.rs` and `src/codegen/template.rs`. | +| Memory/layout | Fast generator tests should assert no overlap between VK, challenge, transcript, quotient, PCS, accumulator, and scratch regions. Keep these as compile-time/layout tests in `src/lowering/layout/memory.rs` (`VerifierMemoryLayout` overlap validation) and `src/lowering/tests.rs`. | | Production artifact checks | `verifyProof` production renders stay `external view`, no `LOG1`, no gas checkpoints, Solidity pragma `^0.8.24`, Cancun/Prague target, runtime size below EIP-170 with margin. | | Wrapper/application binding | Add small mock wrapper contracts that bind expected state root, program ID, chain/domain, caller/action hash, nullifier/nonce. Same proof with wrong wrapper context must reject. | diff --git a/proofs/solidity-verifier/docs/plans/spec-migration.md b/proofs/solidity-verifier/docs/plans/spec-migration.md index f046988df..c10ea6ab2 100644 --- a/proofs/solidity-verifier/docs/plans/spec-migration.md +++ b/proofs/solidity-verifier/docs/plans/spec-migration.md @@ -29,7 +29,7 @@ I'll deliver the migration in N self-contained steps, committing each so we can - Outcome: `cargo check` red until later steps; we get the type universe pinned. ### Step 2 - Transcript replacement -- Throw away the current `src/transcript.rs` (writes uncompressed EIP-2537 G1, single-keccak squeeze). +- Throw away the current transcript implementation (writes uncompressed EIP-2537 G1, single-keccak squeeze). (The transcript now lives in `templates/partials/verifier/TranscriptProofParser.yul`, with proof offsets computed in `src/lowering/abi/`; there is no standalone Rust transcript module.) - Implement a `Keccak256VerifierTranscript` matching `midnight_proofs::transcript::CircuitTranscript` byte-for-byte: - init: `Keccak256::new().update("Domain separator for transcript")` - common(input): hasher.update([1u8]); hasher.update(input) @@ -39,14 +39,14 @@ I'll deliver the migration in N self-contained steps, committing each so we can - Mirror this in Yul (see Step 5). ### Step 3 - ConstraintSystemMeta rewrite -- `src/codegen/util.rs::ConstraintSystemMeta::new` currently inspects halo2_proofs' lookups; replace its lookup section with midnight-proofs' `cs.lookups()` (BatchedArgument with chunks, helpers, multiplicities). +- `src/lowering/encoding/mod.rs::ConstraintSystemMeta::new` currently inspects halo2_proofs' lookups; replace its lookup section with midnight-proofs' `cs.lookups()` (BatchedArgument with chunks, helpers, multiplicities). - Add a trashcan section: `cs.trashcans()` (Argument with selector + constraint expressions). - Track `num_committed_instances` (keep at 0 for poseidon); track `cs.num_simple_selectors()` (midnight-proofs filters fixed evals on this). - New per-row counts: `lookup_chunks_per_arg[]`, `trashcan_count`, `permutation_chunks` (=cols.div_ceil(degree-2)). - `proof_len` calculation now needs: advices, multiplicities, perm prod commitments, lookup helpers + accumulators, trashcans, quotient limbs, evals (committed-inst evals + advice + fixed-non-simple + perm-common + perm-set + lookup-evals + trash), f_com, q_evals (one per point set), pi. ### Step 4 - Evaluator rewrite (partial-eval) -- `src/codegen/evaluator.rs` currently emits Yul for halo2 lookup constraints. Replace `lookup_computations` with a logup emitter that writes: +- `src/lowering/quotient_numerator/yul_emit.rs` currently emits Yul for halo2 lookup constraints. Replace `lookup_computations` with a logup emitter that writes: - For each lookup (per proof): boundary `(l_0 + l_last)*Z` - Per chunk: `h(x) * prod_j(f_j(x)+beta) - sum_j prod_{k!=j}(f_k(x)+beta)` where each `f_j` is theta-compressed - Accumulator: `(Z_next - Z - selector*sum_h)*(t+beta) + m` times `(1 - l_last - l_blind)` @@ -54,7 +54,7 @@ I'll deliver the migration in N self-contained steps, committing each so we can - Gate emitter unchanged shape, but now reads through midnight-proofs `Expression`. ### Step 5 - PCS emitter rewrite -- Replace `src/codegen/pcs.rs` with a `multi_prepare` emitter: +- Replace `src/lowering/kzg/mod.rs` with a `multi_prepare` emitter: - `construct_intermediate_sets`: bucket queries by point sets (in order: advice rotations, perm cur/next/last, lookup x/x_next, trashcan x, fixed rotations, perm common at x, lin com at x). - `q_coms`: per set, MSM-fold commitments by `x1` powers - `q_eval_sets`: per set, eval_set inner product with `x1` powers diff --git a/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md index 39f58aca9..c32299bf9 100644 --- a/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md +++ b/proofs/solidity-verifier/docs/reference/ASKAMA_TEMPLATE_RUST_MAPPING.md @@ -87,7 +87,7 @@ with Midfall transcript and KZG details rendered directly into Yul. | `y`, quotient commitment(s) | Squeezes identity-batching challenge and reads quotient commitment(s) | `verify_algebraic_constraints`: `read_n(transcript, nb_quotient_coms)` before `x` | `proof_layout.rs`, `memory.rs` | | `x` and evaluation scalars | Squeezes opening challenge, range-checks proof evals, stores them in `REVERSED_EVALS_MPTR`, and absorbs them | `verify_algebraic_constraints`: committed-instance evals, advice evals, fixed evals, permutation/common evals, lookup evals, trash evals | `Protocol` eval order, `proof_layout.rs`, `evaluator.rs` | | `x1`, `x2`, `f_com`, `x3`, `q_evals`, `x4`, `pi` | Completes Midfall KZG transcript for multi-opening proof | KZG `multi_prepare` in `../midfall/proofs/src/poly/kzg` | `pcs.rs`, `proof_layout.rs` | -| Lagrange and instance evaluation | Computes `x^n`, `(x^n-1)^-1`, `l_last`, `l_blind`, `l_0`, and the public instance evaluation | `verify_algebraic_constraints`: `domain.l_i_range` and `compute_inner_product` | `templates/contracts/Halo2Verifier.sol`, `memory.rs` | +| Lagrange and instance evaluation | Computes `x^n`, `(x^n-1)^-1`, `l_last`, `l_blind`, `l_0`, and the public instance evaluation; the batch-inversion input run lives in the registered `LAGRANGE_DENOMS_MPTR` region | `verify_algebraic_constraints`: `domain.l_i_range` and `compute_inner_product` | `templates/contracts/Halo2Verifier.sol`, `memory.rs` | | Batched identity numerator reconstruction | Reconstructs `nu_y(x)` from the claimed evals and stores the linearization expected scalar `-nu_y(x)` | `plonk/mod.rs::partially_evaluate_identities`; trace loop for `quotient_numerator` and selector folds | `evaluator.rs`, `quotient/mod.rs`, `QuotientNumeratorBlock.yul` | | Linearization scalar prep | Computes `x_split = x^(n-1)` and `1 - x^n`; prepares selector buckets and quotient-limb scalars | `linearization/verifier.rs::compute_linearization_commitment` | `templates/contracts/Halo2Verifier.sol`, `pcs.rs` | | PCS computation blocks | Constructs point sets, folds evaluations/commitments, computes `f_eval`, `v`, final commitment, and pairing inputs | `CS::multi_prepare` and KZG final guard verification | `src/lowering/kzg/mod.rs` | @@ -391,14 +391,14 @@ comparison against the instrumented Rust verifier. | Quotient constants/program in VK payload | `artifact.rs`, `generate_vk` | Avoids verifier-side immediate constants | VK hash changes when the quotient program changes | | Decoded eval spill buffer | `REVERSED_EVALS_MPTR` | Turns later eval references into cheap `mload`s | Proof scalar order must match protocol metadata exactly | | Off-chain proof repacking | `quotient::RepackedProofLayoutPlan` and ABI docs | On-chain verifier consumes BE scalar words and EIP-2537 G1s directly | Calldata is Solidity-facing, not native Midnight proof bytes | -| Lagrange batch inversion | `batch_invert` helper | Computes all needed inverse denominators with one modexp | Scratch region must not overlap permanent memory | +| Lagrange batch inversion | `batch_invert` helper | Computes all needed inverse denominators with one modexp | Input run (`lagrange_denoms`) and prefix-product scratch (`batch_invert_scratch`) are planner-registered phase regions | | Simple-selector bucket grouping | `compute_linearization_commitment`, `SELECTOR_ACC_MPTR` | Removes simple selector proof evals and groups MSM terms | Selector identity order and final `y` scaling must match Rust | | Fused linearization into PCS MSM | `pcs.rs` | Avoids a standalone production G1MSM for linearization | Trace builds may materialize it only for comparison | | Point-set planning at lowering time | `pcs.rs::intermediate_sets` | Removes dynamic query sorting/grouping from Solidity | Generated verifier is circuit-specialized | | Fewer point sets / dummy queries | `pcs.rs::compute_dummy_queries` | Can reduce KZG point-set work for selected profiles | Proof layout and transcript must include matching dummy evals | | Truncated PCS challenges | `truncated-challenges` feature | Mirrors Midfall KZG challenge truncation where enabled | Only the specified challenges/powers are truncated | | Accumulator pairing batch | `Halo2Verifier.sol` accumulator section | Combines public accumulator pairing with final KZG pairing | Batch randomizer is derived after all four G1 inputs are fixed | -| EIP-2537 gas forwarding and return-size checks | `TemplateConstants`, `layout::precompile` addresses | Forwards `gas()` to avoid chain-specific gas caps while checking call success and exact return sizes | Deployment smoke tests must pass on the target fork/chain | +| EIP-2537 exact gas bounds and return-size checks | `layout::gas`, `GasTemplateConstants`, `layout::precompile` addresses | Forwards the exact EIP-2537/EIP-2565 scheduled cost so a rejecting precompile burns at most one call's scheduled gas (M-2) while checking call success and exact return sizes | Bounds are the spec schedule, not hand-tuned caps; an upward repricing fork requires regenerating; deployment smoke probes forward the same bounds and fail fast on repriced chains | | Gas checkpoints | `RenderDiagnostics { gas_checkpoints: true, .. }` | Gives stable section-level gas deltas | Not a `view` verifier; profiling only | ## Trace Coverage diff --git a/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md new file mode 100644 index 000000000..6ef6d5337 --- /dev/null +++ b/proofs/solidity-verifier/docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md @@ -0,0 +1,185 @@ +# Deployment, Incident Response, and Accepted Risks + +Closes review item **L-9** (no incident-response or migration story) from +`docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md`, and records the wrapper +obligations that `AUDIT.md` TA-8 places on integrators. + +## 1. What the deployed verifier is — and is not + +A generated `Halo2Verifier` is **stateless and immutable**: no owner, no +pause, no upgrade path, no `sstore`/`delegatecall`/`selfdestruct`. That is +deliberate — an on-chain admin lever on a verifier is itself an attack +surface. Two consequences follow, and both land on the application wrapper: + +1. **There is no on-chain mitigation for a post-deployment soundness bug.** + The only lever is the wrapper above the verifier. +2. **A valid proof is valid everywhere, forever.** The transcript starts at + `vk_digest`; nothing binds `block.chainid`, the verifier address, or the + deployment. `verifyProof == true` is a statement about the proof and the + circuit, not about *this chain* or *this application*. + +## 2. Wrapper obligations (REQUIRED, not advisory) + +Every production integration MUST satisfy all three; a wrapper that +hardcodes the verifier as `immutable`/`constant` and treats +`verifyProof == true` as authorization has **no recovery path and +unconditional cross-chain replay**: + +- **W-1 — Replaceable verifier address.** The wrapper holds the verifier + behind an updatable (governed/timelocked) address so a re-rendered + verifier can be swapped in. +- **W-2 — Wrapper-held pause.** The wrapper can stop accepting proofs while + an incident is investigated. The verifier itself cannot. +- **W-3 — Domain binding + replay protection.** The statement the circuit + proves (or the wrapper's own checks over the public instances) MUST bind: + `block.chainid`, the wrapper (or verifier) address, and an + application-level anti-replay value (nullifier, nonce, or consumed-state + root). Without this, any accepted proof can be replayed on every chain + and against every deployment of the same bytecode. + +- **W-4 — Call it so a wrong address cannot read as success (MF-5).** A + low-level `verifier.staticcall(...)` returns `ok = true` with empty + returndata when the target has **no code** — a wrong address, a wrong + chain, or a wrapper configured before deployment reads as a valid proof. + Call through the typed interface (Solidity ≥0.8 inserts the `extcodesize` + check), or, on any low-level path, require `returndatasize() >= 32` AND a + decoded `true`. If the wrapper uses `try/catch`, every catch branch is a + rejection: the verifier's failures are custom errors, so `catch Error(string)` + and `catch Panic(uint)` will not match them — use `catch (bytes memory)` or a + bare `catch`. +- **W-5 — Bind the accumulator's meaning, not just its validity (MF-9).** For + IVC renders, the verifier checks that the carried accumulator points decode + canonically, are in the subgroup, and satisfy the batched pairing equation. + It does NOT check that they are non-trivial or that they continue *your* + chain: the canonical identity encoding `(O, O)` is a well-formed accumulator + and passes by construction. Any "this accumulator continues the expected + fold" rule belongs to the circuit or the wrapper. + +`verifyProof`'s NatSpec states the same split: the raw verifier checks the +proof against the pinned VK and nothing else. + +## 3. Deployment record + +For every production deployment, record and publish: + +| Item | Source | +| --- | --- | +| `BUILD_ID` and each preimage component | the deployed contract's `BUILD_ID` constant; components below | +| Feature profile string | `build.rs` export baked into the generator (`SOLIDITY_VERIFIER_FEATURES`) | +| `vk_digest`, VK runtime length/codehash | generated constants | +| SRS fingerprint + asset SHA-256 + ceremony reference | `REPRODUCIBLE_BUILDS.md` ("SRS Provenance") | +| Provenance tag preimage | the `RenderOptions::provenance` input, e.g. `keccak256("commit=,dirty=")` — deployment builds MUST set it | +| Compiler identity + flags | `REPRODUCIBLE_BUILDS.md` (pinned solc SHA-256, `--optimize-runs`) | +| Artifact manifest | `scripts/generate_artifact_manifest.sh` output | +| Target security level and accepted risks | §5 below | + +`BUILD_ID` exists precisely so "which of our deployed verifiers has the +affected codegen?" is answerable from chain state during an incident. + +## 4. Incident response and migration playbook + +**On a suspected soundness/liveness bug in a deployed verifier:** + +1. **Pause** intake at the wrapper (W-2). Verifier-level mitigation does not + exist by design. +2. **Scope** the blast radius: enumerate deployments and read each + `BUILD_ID`; match against the deployment records to find which builds + carry the affected generator code, feature profile, or SRS. +3. **Assess replay exposure**: anything accepted by an affected verifier + must be treated per W-3 — if the statement was not domain-bound, assume + cross-chain/cross-deployment replay of every historical proof. +4. **Fix and re-render**: land the generator fix, re-run the full gated + suite and the IVC bench, regenerate artifacts, record new hashes and a + new `BUILD_ID` (with a fresh provenance tag). +5. **Migrate**: deploy VK first, then verifier (the constructor fails closed + on a wrong VK), verify on-chain `BUILD_ID` matches the record, switch the + wrapper pointer (W-1), unpause. +6. **Retire** the old verifier in the deployment record (it cannot be + destroyed on-chain); wrappers must never point back at it. + +**On a fork of the chain you are deployed to — check this first (MF-1).** +The verifier forwards exact scheduled gas to every precompile, so an upward +repricing does not degrade: it bricks `verifyProof` outright (liveness, never +soundness). The canonical symptom is a **sudden, total `PrecompileFailed` +rate immediately after a fork activation**, on proofs that verified the day +before and still verify against the native Rust verifier. Triage: + +1. Compare the fork's precompile schedule against the deployed constants + (`G1ADD_GAS`, `G1MSM_GAS_*`, `PAIRING_GAS_2PAIR`, `MODEXP_GAS`). +2. If any scheduled cost now exceeds the deployed bound, that is the cause; + re-render (the generator takes the maximum over live schedules) and + migrate per the steps above. There is no wrapper-side mitigation. + +Worked example: EIP-7883 (Fusaka, mainnet 2025-12-03, testnets earlier) +removed the `/ 3` divisor from modexp pricing, taking the verifier's frame +from 1360 to 4064 gas. An artifact rendered with exact bounds but before the +MF-1 fix carries `MODEXP_GAS = 1360` and reverts every proof on any +post-Fusaka chain. Deployment of NEW artifacts now fails fast in the +constructor probes for every precompile the runtime calls, modexp included. + +**The exposure is exactly the artifacts that carry exact bounds.** Note what +that implies for `deployments/sepolia/moonlight-wrap`: it is NOT affected. It +predates the exact-gas hardening entirely — all 17 of its `staticcall`s +forward `gas()`, including its three modexp sites — so a repricing is simply +absorbed from the caller's remaining gas. (Verified by reading the recorded +source, whose recompiled runtime matches `runtimeCodeHash` in +`deployment.json` byte-for-byte apart from the two immutable `AUTHORIZED_VK` +slots, so the recorded source is genuinely what is deployed.) + +That is the trade-off worth stating plainly, because it is easy to get +backwards: **exact-gas forwarding is what creates repricing fragility.** The +older `gas()`-forwarding renders survive any upward repricing but are exposed +to the DoS that exact bounds were introduced to close (M-2) — a malformed +proof point burns 63/64 of the transaction budget instead of one scheduled +call. Neither property is free; the constructor probes exist so the fragility +the current design accepts is caught at deployment rather than in production. + +## 5. Accepted risks (deployment owner sign-off) + +### 5.1 Keccak transcript has no domain separation (I-5) + +The Keccak transcript absorbs raw concatenated bytes with no +personalisation string or `COMMON`/`CHALLENGE` tags (the Blake2b transcript +upstream has both). Not exploitable in this protocol: every absorb is +fixed-length with pinned counts, so no two distinct valid inputs produce +the same byte stream, and `vk_digest` provides cross-circuit separation. +It cannot be fixed verifier-side — adding tags would break every proof from +the midnight-proofs prover; this is the same class of upstream protocol +change as M-4 (`vk_digest` coverage), and is **accepted as a documented +defence-in-depth gap** until the transcript changes upstream. + +### 5.2 `vk_digest` coverage (M-4) — deferred upstream decision + +`vk_digest` does not cover the SRS points, quotient VM program, accumulator +schema, or feature profile. Compensating controls: build-time SRS tau +binding, VK codehash pin, generated-constant cross-checks, and `BUILD_ID` +(which does cover all of the above, off-transcript). Widening the digest is +a prover-affecting protocol change, explicitly out of scope by owner +decision (2026-08-13). + +## 6. Reading a revert (MF-4) + +`verifyProof` is success-or-revert: it returns `true` or reverts with one of +the typed errors below. The taxonomy exists so the first question during an +incident — *is this the chain, the build, or the proof?* — is answerable from +the 4-byte selector alone, without a trace. + +| Error | Selector | Class | First thing to check | +| --- | --- | --- | --- | +| `BadCalldataShape()` | `0x1b99e37c` | Caller | Heads, lengths, and EXACT `calldatasize`. A calldata-appending relayer (ERC-2771, multicall, paymaster) cannot call this contract directly. | +| `VkMismatch()` | `0xa447d73e` | Deployment | The pinned VK address no longer has the expected runtime length/codehash, or a VK header word disagrees with the generated constants. | +| `NonCanonicalScalar()` | `0x77530042` | Proof | A public instance or proof scalar is `>= r`. Usually an off-chain repacking bug, not an attack. | +| `BadPointEncoding()` | `0xf27905ec` | Proof | A proof point violates the EIP-2537 padding/field bounds, or an accumulator public input failed canonical decoding. | +| `PrecompileFailed()` | `0x84e81692` | **Chain** | A precompile could not run: missing, repriced above the forwarded bound (see §4), or short-returning. Also raised when G1MSM *rejects* a proof point as off-curve/out-of-subgroup — the precompile is the validator, so that rejection surfaces here by design. | +| `ProofRejected()` | `0xc3b0d8cd` | Proof | The pairing ran and returned != 1, or a Lagrange denominator was zero (the squeezed `x` hit a domain point, probability ~n/r). | +| `QuotientProgramInvalid()` | `0x3cc81b89` | **Build** | The VK-pinned quotient program violated a structural invariant. Not reachable with a well-formed artifact; treat as a generator bug. | +| `MemoryLayoutViolated()` | `0xc9888d23` | **Build** | solc's stack-spill reservation overlaps the generated layout. The artifact was compiled off the pinned toolchain and can never verify anything; redeploy from the pinned `(version, --optimize-runs)` pair. | + +Two rules of thumb: + +- **Chain/Build classes are total, not probabilistic.** They fail every call, + including calls that verified yesterday. A sudden all-or-nothing failure + rate points here; a per-proof failure rate points at the Proof class. +- **A revert is never an accept.** Every path above fails closed. There is no + configuration in which `verifyProof` returns `false` — see W-4 for why a + wrapper must not treat a bare `staticcall` success as verification. diff --git a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md index 14d1ef4a8..60afe2cb6 100644 --- a/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md +++ b/proofs/solidity-verifier/docs/reference/HALO2_MIDNIGHT_VERIFIER_SPEC.md @@ -65,11 +65,14 @@ proofs using: Supported execution target: an Ethereum-compatible Cancun-or-newer EVM with `MCOPY` and Prague/EIP-2537 BLS12-381 precompiles at exactly the addresses -above, implementing the EIP-2537 input encodings, subgroup checks, return sizes, -and enough gas capacity for the generated full-size calls. The repository -CI/dev runner exercises this target through Prague-spec `revm`; deployers on -L2s, forks, or alt-EVMs must run the same precompile conformance tests against -their target chain before treating the verifier as production-safe. +above, implementing the EIP-2537 input encodings, subgroup checks, return +sizes, and the EIP-2537/EIP-2565 gas schedule (the generated call sites +forward exactly the scheduled cost — see section 15). The repository CI/dev +runner exercises this target through Prague-spec `revm`; deployers on L2s, +forks, or alt-EVMs must run the same precompile conformance tests against +their target chain before treating the verifier as production-safe. A chain +that charges MORE than the EIP-2537 schedule fails the constructor smoke +probes at deployment. The generated verifier is not a generic reusable verifier. It is circuit-specialized. Circuit metadata, proof read order, quotient identity @@ -532,6 +535,16 @@ The VK codehash pins: The generated verifier implements the Midfall Keccak transcript. +**No domain separation, by protocol (audit I-5, accepted gap).** The Keccak +transcript hashes raw concatenated bytes with no personalisation string and +no absorb/squeeze tags, exactly matching the midnight-proofs prover. This is +safe here because every absorb is fixed-length with generated, re-checked +counts (no two distinct valid inputs share a byte stream) and `vk_digest` +separates circuits — but it is a missing defence-in-depth layer. It cannot +be added verifier-side without rejecting every real proof; like M-4, it is +an upstream transcript decision. Recorded in +`docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md` §5.1. + ### 7.1 Transcript State The transcript state is a byte buffer. @@ -1200,6 +1213,17 @@ eval_s = proof_q_eval[s] * den_inv All inversions are in `Fr`. The implementation batch-inverts the `dx_j` and `lbasis_j` values for each non-singleton set. +**Zero-denominator semantics (intended, not incidental).** Every inversion +path in the generated verifier — `scalar_inv`, `batch_invert`, and the +Lagrange denominators — fails closed on a denominator congruent to zero +mod `r`: the call either reverts or forces `success := 0`, which the next +section boundary turns into a revert. In particular, a transcript challenge +`x` landing on a domain root of unity (probability ~`n/r`, negligible) or +`x3` colliding with a rotation point makes the proof REJECT with a revert +rather than being accepted, retried, or specified around. This mirrors the +native verifier, which would fail the same proofs, and is the specified +behavior (audit item TA-7). + Fold with `x2`: ```text @@ -1550,9 +1574,14 @@ rules. The generated verifier constructor runs smoke tests: - `MCOPY` one-word round trip in constructor scratch. -- `G1ADD(identity, identity) -> identity`. +- `modexp(2, r-2, r) == 2^-1`, checked as `mulmod(result, 2, r) == 1`, at the + pinned `MODEXP_GAS` bound (MF-1). +- `G1ADD(identity, identity) -> identity`, and the known answer `G1ADD(G, G) == 2G`. +- `G1MSM([2]*G) == 2G`, and a **negative** probe: a point on the curve but + outside the r-order subgroup must be rejected. - Largest generated `G1MSM` input with identity/zero terms -> identity. -- `PAIRING_CHECK` over two identity `(G1, G2)` pairs -> true. +- `PAIRING_CHECK` over two identity `(G1, G2)` pairs -> true, plus the known + answers `e(G,G2)e(-G,G2) == 1` and `e(G,G2)e(G,G2) != 1`. Deploy only on forks/chains where EIP-2537 and `MCOPY` are available with the exact addresses, encodings, subgroup checks, return-size behavior, and enough @@ -1567,11 +1596,28 @@ Every EIP-2537 call checks: - Exact return-data size. - For pairing, returned word is 1. -EIP-2537 calls forward `gas()` rather than rendering chain-specific gas caps. +EIP-2537 and modexp calls forward exact scheduled costs (generated constants +`G1ADD_GAS`, `G1MSM_GAS_*`, `PAIRING_GAS_2PAIR`, `MODEXP_GAS`; model in +`src/lowering/layout/mod.rs::gas`), NOT `gas()`. For modexp the rendered bound +is the MAXIMUM over the live schedules -- EIP-2565 prices the verifier's +32/32/32 frame at 1360, EIP-7883 (Osaka/Fusaka) removes the `/ 3` divisor and +prices it at 4080 -- because a bound below the chain's price does not degrade +gracefully: the fixed-gas `staticcall` runs the precompile out of gas and every +proof reverts `PrecompileFailed` (MF-1). Over-forwarding on a pre-Osaka chain +costs nothing on success; unused gas is returned. A +rejecting precompile consumes everything forwarded to it, so exact bounds cap +what a malformed proof point can burn at the scheduled cost of the single +failing call instead of 63/64 of the transaction budget (M-2). The bounds are +the spec-guaranteed worst case per EIP-2537's DDoS-protection rationale, so +they are sufficient on any conformant chain; a future fork that reprices +these precompiles upward requires regenerating and redeploying the verifier. + The constructor also smoke-tests the largest generated G1MSM input length and -the runtime two-pair KZG pairing input size with identity data, so deployment -fails early when the target chain/fork cannot execute the verifier's -worst-case precompile shapes under the supplied deployment gas. +the runtime two-pair KZG pairing input size with identity data — forwarding +the same exact gas bounds — so deployment fails early when the target +chain/fork cannot execute the verifier's worst-case precompile shapes at the +generated schedule, including chains whose precompile gas schedule was +repriced upward. ## 16. Codegen Configuration diff --git a/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md b/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md index 63e06a6b2..64e5c6230 100644 --- a/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md +++ b/proofs/solidity-verifier/docs/reference/QUOTIENT_EVALUATOR_9KB_BYTECODE.md @@ -104,8 +104,16 @@ normal ABI call. The largest win was the compact quotient VM. Instead of rendering most identities as Yul source, the generator lowers them to -a small bytecode language in `src/lowering/quotient/mod.rs`. The runtime consumer -is `templates/partials/quotient_numerator/QuotientNumeratorBlock.yul`. +a small bytecode language in `src/lowering/quotient_numerator/vm/mod.rs`. The +runtime consumer is +`templates/partials/quotient_numerator/QuotientNumeratorBlock.yul`. + +Every emitted program is certified before it can be pinned into a verifying key: +`src/lowering/quotient_numerator/vm/certify.rs` re-executes the finalized +bytecode with the independent interpreter in +`src/lowering/quotient_numerator/vm/reference.rs` and compares each identity +against direct evaluation of the expression tree it was lowered from, and +against a second build with the limb superinstructions disabled. The VK payload carries: diff --git a/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md b/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md index 2134df9a2..8576f2dea 100644 --- a/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md +++ b/proofs/solidity-verifier/docs/reference/REPRODUCIBLE_BUILDS.md @@ -6,12 +6,72 @@ bytecode: - Rust toolchain: `rust-toolchain.toml` - Midfall dependency revision: `53dc872f495104046d96bdac0a690f903dc0c537` -- Solidity compiler: `solc 0.8.30+commit.73712a01` -- Solidity compile flags: `--bin --optimize --via-ir --evm-version cancun --no-cbor-metadata` +- Solidity compiler: `solc 0.8.30+commit.73712a01`, installed and + SHA-256-verified by `scripts/install_pinned_solc.sh`. Official binary + hashes from `binaries.soliditylang.org//list.json`: + - `linux-amd64`: + `f3e987dc6ecebd4bd350c48edcbc320b46cf9e3109bd3fc3d88f1acaf4c428f7` + - `macosx-amd64` (also used on Apple Silicon via Rosetta 2): + `738dcdc6afddeb505ee4e4ef24f1c1fdba2b8c924e614cbbf5801a5b062dd683` +- Solidity compile flags: `--bin --optimize --optimize-runs --via-ir + --evm-version cancun --no-cbor-metadata`, where `` defaults to `200` + (`DEFAULT_OPTIMIZE_RUNS` in `src/evm.rs`, override with + `SOLC_OPTIMIZE_RUNS`); the IVC bench profiles below use `runs: 1`. + + **`--optimize-runs` is bytecode-affecting and must be recorded with every + artifact hash.** It is also deployability-affecting: solc 0.8.30 at + `runs=100000` emits a verifier over the EIP-170 24,576-byte runtime limit, + i.e. an undeployable contract that compiles without complaint. Repository-local `.cargo/config.toml` path overrides are intentionally not used. All Midfall crates are resolved from the pinned git revision in `Cargo.toml`. +## Provenance Identities + +Three different commit stamps appear across the audit and fixture documents. +They index different things; every recorded stamp should say which of these +it is: + +| Stamp | Identifies | +| --- | --- | +| `53dc872f495104046d96bdac0a690f903dc0c537` | The **Midfall dependency** revision pinned in `Cargo.toml` (also the source of the comment corpus). | +| `a096e71746e401404f250817ca4e857bac1eef56` | **This repository** at the time the review packet (`docs/audit/REVIEW_PACKET.md`) was assembled. | +| `3fb6d84` | **This repository** at the time the moonlight-wrap replay fixture (`fixtures/moonlight-wrap/`) was rendered. | + +## SRS Provenance + +`NEG_S_G2_BASE` — the element every soundness guarantee of a deployed +verifier rests on — is derived from the SRS at build time. Build-time code +(`src/lowering/vk.rs`) proves the SRS is internally consistent (G1/G2 bases +canonical, `s_g2` pairing-bound to the tau underlying `g_lagrange`), and the +gated test `midnight_srs_assets_bind_s_g2_to_lagrange_tau` runs the same +check directly against the asset files. What internal consistency cannot +prove is *which ceremony* an asset came from; that link is this record. + +Ceremony reference: the Midnight trusted-setup ceremony, published at +. Its +`MIDNIGHT_SRS_CATALOG.md` is the authoritative checksum table and also +documents a cargo tool for verifying an asset against the ceremony's +powers-of-tau transcript. *(Citation added 2026-08-12 from the reference in +`zk_stdlib/src/utils/plonk_api.rs`; deployment owners should confirm this is +the ceremony they intend to trust.)* + +Recorded asset hashes (`scripts/record_srs_provenance.sh`, 2026-08-12; +Midnight rows verified byte-identical against the official catalog above): + +| Asset | Bytes | SHA-256 | Matches official catalog | +| --- | ---: | --- | --- | +| `midnight-srs-2p19` | 100,663,684 | `8e8dc15c4362f05c912f1e770559a3945db3e58a374def416ed5d3e65ad5b10e` | yes (2026-08-12) | +| `midnight-srs-2p20` | 201,326,980 | `1cc62978558fdc1e445cd70cfd9a86ec3c2e2151b6d74811232d37faf9133ff1` | yes (2026-08-12) | +| `bls_filecoin_2p19` | 100,663,684 | `0574a536c128142e89c0f28198d048145e2bb2bf645c8b81c8697cba445a1fb1` | n/a (Filecoin SRS, test fixtures only) | + +Re-run the script before any deployment build and compare against this +table; then run the tau-binding test: + +```bash +HALO2_SOLIDITY_RUN_EVM_TESTS=1 cargo test --release --features evm midnight_srs_assets_bind_s_g2_to_lagrange_tau +``` + ## Canonical IVC Bench Command The current default IVC Solidity bench uses the multi-limb outer decider proof @@ -83,8 +143,12 @@ The proof repacked from `4,912` compressed bytes to `7,392` padded bytes, with ## Recorded Legacy Multi-Limb Runtime Hashes -The concrete hashes below are the latest recorded default multi-limb profile in -this repository. They were generated with the fixed default quotient codegen shape: +The concrete hashes below are the latest recorded default multi-limb profile +in this repository (recorded 2026-08-13, after the exact precompile gas +bounds, quotient-VM operand clamps (P12), typed errors (P4), BUILD_ID (P10), +and the alpha vk-binding (I-7); the tracked +`target/ivc-keccak-solidity-dump/` sources match this run). They were +generated with the fixed default quotient codegen shape: ```bash scripts/run_ivc_bench.sh \ @@ -103,15 +167,25 @@ Published deployed-runtime hashes: | Artifact | Runtime bytes | Runtime `keccak256` | | --- | ---: | --- | -| `Halo2Verifier` | 12,061 | `0xf0d2433a3142294afb4d9a9434d623b8f00bbea212380777b5aee197e79b1454` | -| `Halo2VerifyingKey` | 14,016 | `0x0f858e789c9d52f7e11beb96dd39fa30712c42f629c3505be9cceef3225119a0` | -| `Halo2QuotientEvaluator` | 23,221 | `0x1f8acc8aa363e10d031e9e56dd4180a70c714330fa466d3423f9ce1d4e1f00b2` | +| `Halo2Verifier` | 12,637 | `0x6df65ec939553efef2dadffdab078bfb424d10307a32dddd84866cf94d29b215` | +| `Halo2VerifyingKey` | 17,025 | `0x67bac137fa7e479c25b63324812752e4b6e13d9841d5bf83c322170bf91c0f88` | +| `Halo2QuotientEvaluator` | 9,790 | `0x7e72c7c5d6fe845370d9431aaa590ab2cd62ca703c3d5b2a862bdb9937195814` | + +Total deployed runtime bytes: `39,452`. + +The same run accepted the final IVC Keccak proof on-chain in `1,365,883` gas +(gas-checkpoint diagnostic profile). The proof repacked from `5,056` +compressed bytes to `7,776` padded bytes, with `8,356` bytes of calldata. -Total deployed runtime bytes: `49,298`. +Measured cost of the P12 runtime operand clamps: the quotient-VM section +("batched identity numerator reconstruction") went from `314,530` to +`374,481` gas (+59,951, +19.1% of that section, ~+4.6% of the transaction); +every other section is unchanged (PCS block 5 stays at `533,488`). -The same run accepted the final IVC Keccak proof on-chain in `1,399,268` gas. -The proof repacked from `5,056` compressed bytes to `7,776` padded bytes, with -`8,356` bytes of calldata. +Previous recordings, for comparison: 2026-08-13 pre-hardening — verifier +12,454 / VK 17,025 / evaluator 9,552 bytes (39,031 total), accepted in +`1,306,084` gas; 2026-07-era artifact set — verifier 12,061 / VK 14,016 / +evaluator 23,221 bytes (49,298 total), accepted in `1,399,268` gas. Compared with this multi-limb profile, outer single-H removes three quotient G1 commitments: proof size drops by `144` compressed bytes and `384` padded bytes, diff --git a/proofs/solidity-verifier/fixtures/ivc/Halo2QuotientEvaluator.sol b/proofs/solidity-verifier/fixtures/ivc/Halo2QuotientEvaluator.sol new file mode 100644 index 000000000..30464e1e4 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/Halo2QuotientEvaluator.sol @@ -0,0 +1,1628 @@ +// SPDX-License-Identifier: CC0-1.0 +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; + +/// @title Split Halo2 quotient numerator evaluator. +/// @notice Reconstructs the scalar side of the linearization query for a generated verifier. +/// @dev This is the split-out implementation of the expensive +/// `partially_evaluate_identities` / `compute_linearization_commitment` side +/// from the Midfall Rust verifier: +/// - `midfall/proofs/src/plonk/mod.rs::partially_evaluate_identities` +/// - `midfall/proofs/src/plonk/linearization/verifier.rs::compute_linearization_commitment` +/// - `midfall/proofs/src/plonk/{permutation,logup,trash}.rs` +/// @dev The main verifier has already parsed calldata, checked proof scalar +/// ranges, sampled Fiat-Shamir challenges, loaded the VK payload, and computed +/// local Lagrange/public-input values before making the staticcall. +/// +/// Instead of receiving structured Solidity arguments, the evaluator receives +/// the verifier's memory frame as raw calldata: +/// +/// calldata[0..QUOTIENT_FRAME_LEN) +/// == memory[QUOTIENT_FRAME_BASE..QUOTIENT_FRAME_BASE+QUOTIENT_FRAME_LEN) +/// +/// The fallback copies that frame back into the same generated memory +/// addresses. All constants below are therefore memory addresses inside that +/// copied frame, not ABI offsets. +/// +/// Output is a compact fixed frame consumed by Halo2Verifier: +/// +/// word 0: QUOTIENT_MAGIC, a generated version/magic guard +/// word 1: linearization_expected_eval +/// word 2..: simple-selector accumulator scalars +/// +/// This contract reconstructs the Rust verifier's y-batched identity numerator +/// nu_y(x) and returns the linearization expected scalar -nu_y(x). It does not +/// evaluate or trust a quotient scalar h(x). +/// +/// The quotient limb commitments are handled by Halo2Verifier on the commitment +/// side as (1 - x^n) * sum_i x_split^i * Q_i. That is why this scalar side is +/// -nu_y(x), not h(x) = nu_y(x) / (x^n - 1). +/// +/// See docs/QUOTIENT_NUMERATOR_EVALUATOR.md for the full Rust/Solidity mapping. +contract Halo2QuotientEvaluator { + // BLS12-381 scalar field modulus. All arithmetic in this contract is over + // Fr and uses addmod/mulmod with this modulus. + uint256 internal constant FR_MODULUS = + 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // Start of the copied verifier-key payload in memory. The VK payload also + // carries the compact quotient VM constant/program tables used by the + // included numerator block. + uint256 internal constant VK_MPTR = 0x3680; + + // Fiat-Shamir challenge slots. Halo2Verifier sampled these in transcript + // order before the external call. The evaluator only reads them. + uint256 internal constant CHALLENGE_MPTR = 0x7900; + uint256 internal constant THETA_MPTR = 0x7900; + uint256 internal constant BETA_MPTR = 0x7920; + uint256 internal constant GAMMA_MPTR = 0x7940; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x7960; + uint256 internal constant Y_MPTR = 0x7980; + uint256 internal constant X_MPTR = 0x79a0; + uint256 internal constant X1_MPTR = 0x79c0; + uint256 internal constant X2_MPTR = 0x79e0; + uint256 internal constant X3_MPTR = 0x7a00; + uint256 internal constant X4_MPTR = 0x7a20; + + // Common polynomial values at x. Halo2Verifier computes these once after + // sampling x and places them in the frame so the numerator block can share + // the exact Rust verifier inputs. + uint256 internal constant X_N_MPTR = 0x7c40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x7c60; + uint256 internal constant L_LAST_MPTR = 0x7c80; + uint256 internal constant L_BLIND_MPTR = 0x7ca0; + uint256 internal constant L_0_MPTR = 0x7cc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x7ce0; + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x7d00; + + // Proof evaluation table. Values are already decoded as canonical Fr words + // by Halo2Verifier. The generated numerator code indexes this table by the + // same query order as the Rust verifier. + uint256 internal constant REVERSED_EVALS_MPTR = 0x9480; + + // Scratch/output region for simple-selector linearization accumulators. + // The numerator block writes one bucket per simple selector, then the + // fallback copies those buckets into the compact return frame. + uint256 internal constant SELECTOR_ACC_MPTR = 0xb140; + // Callee-local scratch for trace hooks. Trace-enabled verifier builds call + // this evaluator with CALL so quotient identity logs can be compared with + // the native Rust trace. Production verifier builds keep using STATICCALL + // and render this evaluator without trace hooks. + uint256 internal constant TRACE_U256_MPTR = 0x1000; + uint256 internal constant QUOTIENT_OUTPUT_MPTR = 0x1000; + + // External-call frame metadata. The main verifier calls this contract with + // exactly QUOTIENT_FRAME_LEN bytes starting at + // QUOTIENT_FRAME_BASE, then checks the return length and QUOTIENT_MAGIC. + uint256 internal constant QUOTIENT_FRAME_BASE = 0x3680; + uint256 internal constant QUOTIENT_FRAME_LEN = 0x6ac0; + uint256 internal constant QUOTIENT_OUTPUT_LEN = 0x0180; + uint256 internal constant QUOTIENT_MAGIC = 0x00000000000000000000000000000000000000000000000051554556414c0001; + + /// @notice Evaluate the generated quotient numerator block for one verifier memory frame. + /// @dev Calldata is exactly the raw frame, not ABI-encoded arguments. Returns `QUOTIENT_MAGIC`, the linearization expected eval, and selector buckets. + /// @dev This fallback also uses generated absolute memory addresses and + /// returns directly from assembly. Its compact return frame starts at + /// `0x80`, preserving Solidity's reserved memory words. + fallback() external { + assembly ("memory-safe") { + // Reject malformed calls. This contract is not a general-purpose + // ABI endpoint; accepting partial or shifted frames would make the + // generated memory addresses point at the wrong data. + if iszero(eq(calldatasize(), QUOTIENT_FRAME_LEN)) { revert(0, 0) } + + // Rehydrate the verifier memory image. From this point onward the + // generated Yul can use the same MPTR constants as the monolithic + // verifier path. + calldatacopy(QUOTIENT_FRAME_BASE, 0, QUOTIENT_FRAME_LEN) + + let r := FR_MODULUS + + // This included block is the main body of the evaluator. It: + // 1. evaluates gate/permutation/lookup/trash identities in the + // same order as Rust `partially_evaluate_identities`; + // 2. y-batches fully evaluated identities into + // quotient_eval_numer; + // 3. y-batches simple-selector identities into + // SELECTOR_ACC_MPTR buckets; + // 4. writes -quotient_eval_numer to QUOTIENT_EVAL_MPTR. + // + // Depending on codegen settings, some identities are native Yul + // callbacks and the rest are executed by the compact q_program VM + // stored in the copied VK payload. + // + // The upstream Rust comments call out that simple multiplicative + // selectors do not appear as normal proof eval scalars. The Yul + // block mirrors that rule by accumulating those identities into + // SELECTOR_ACC_MPTR buckets for later multiplication by fixed + // selector commitments, while fully evaluated identities contribute + // to the negated expected scalar. // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } + + // Optional quotient helper functions. Each one is rendered only + // when the Rust lowering pass recognized the corresponding + // expression shape in this generated verifier. They are pure Fr + // helpers and share the same FR_MODULUS as the surrounding + // numerator block. + // VK-specialized identity helper for Poseidon S-box terms. + // + // Rust source shape: + // circuits/src/hash/poseidon/poseidon_chip.rs::sbox + // full_round_gate / partial_round_gate + // circuits/src/hash/poseidon/round_skips.rs::RoundId + // + // The Rust verifier only sees this as an Expression tree from + // `vk.cs.gates`; the generator emits q_pow5 after recognizing five + // equal multiplicative factors. It is a codegen shortcut for x^5, + // not a separate verifier rule. + function q_pow5(x) -> z { + let q_r := FR_MODULUS + let x2 := mulmod(x, x, q_r) + z := mulmod(x, mulmod(x2, x2, q_r), q_r) + } + // =============================================================== + // Batched identity numerator / linearization target. + // + // This block does not evaluate the quotient polynomial h(x), and + // the proof does not provide an h(x) scalar to trust. Instead it: + // + // 1. Reconstructs the y-batched constraint numerator nu_y(x) + // from the alleged polynomial evaluations read after the + // transcript sampled x. + // 2. Stores -nu_y(x) as the expected opening scalar for the + // linearized commitment. + // + // The commitment side is built in the next block from the quotient + // limb commitments as (1 - x^n) * Σ_i x_split^i * Q_i, plus any + // simple-selector commitments. The PCS check later binds that + // linearized commitment to this expected scalar at x. + // + // Rust source-of-truth: + // - verifier.rs reads quotient commitments, samples x, then + // reads/computes all evaluations used below. + // - mod.rs::partially_evaluate_identities returns identities in + // gate, permutation, lookup, trash order. + // - linearization/verifier.rs::compute_linearization_commitment + // reverse-folds those identities by powers of y, sends + // simple-selector identities to selector commitment scalars, + // and subtracts fully-evaluated identities into expected_eval. + // + // This template is shared by the monolithic and external quotient + // paths. In the external path, Halo2QuotientEvaluator first copies + // the verifier memory frame into the same generated addresses. + // + // Runtime inputs expected to exist before this block starts: + // - `r` is the BLS12-381 scalar-field modulus. + // - Y_MPTR holds the quotient batching challenge y. + // - X_MPTR, L_*_MPTR, INSTANCE_EVAL_MPTR, and + // REVERSED_EVALS_MPTR hold values parsed or derived by the + // main verifier after the transcript sampled x. + // - VK_MPTR holds the pinned VK payload; in compact mode that + // payload includes the quotient constant table and bytecode. + // + // Runtime outputs written by this block: + // - QUOTIENT_EVAL_MPTR receives the scalar expected opening for + // the linearized commitment, namely -nu_y(x). + // - SELECTOR_ACC_MPTR[0..num_simple_selectors) receives one + // linearization scalar per generated simple selector. + // + // Line-by-line reading conventions used below: + // + // * Every runtime value is one canonical Fr element stored in a + // 256-bit EVM memory word. The small integer operands decoded + // from q_program are never field values; they are pointers, + // constant-table slots, selector indexes, offsets, or counts. + // + // * `mload(ptr)` is the only way the VM turns a small pointer + // operand into a real 255-bit field element. The value loaded + // from memory is then combined with `addmod(..., r)` or + // `mulmod(..., r)`, so every arithmetic line is reduced modulo + // the BLS12-381 scalar-field order. + // + // * `q_top` is the cached top of the VM operand stack. When an + // opcode needs to push while `q_top` is already live, the old + // value is written to `q_sp` and `q_sp` is advanced by one + // word. Binary `ADD`/`MUL` move `q_sp` back by one word and + // combine that spilled value with `q_top`. + // + // * Identity boundaries are explicit. Expression opcodes leave + // one value in `q_top`; `FOLD_MAIN` or `FOLD_SELECTOR` consumes + // it and advances the global y-batch position. Native callback + // opcodes are only emitted at empty-stack boundaries and run + // generated Yul that performs the same fold side effects. + // + // * The generated Solidity source intentionally emits comments + // before opcode cases. Those comments are documentation only: + // they do not affect bytecode, but they make rendered verifier + // assembly readable without jumping back to Rust codegen. + // =============================================================== + { + // Compact quotient-program mode. + // + // The largest identity expressions are not all emitted as + // unrolled Yul. Instead, most arithmetic is encoded as a small + // q_program bytecode stored in the VK payload. This block + // interprets that program, while selected heavy identities may + // still be emitted as native callbacks for gas. + // + // Compact mode is a code-size trade: short bytecode operands + // name already-planned memory slots, and the interpreter turns + // those names into Fr arithmetic. The opcode stream is fully + // generated and pinned by the VK/runtime codehash; no proof + // calldata can alter control flow. + // Load the quotient batching challenge used by every fold. + let y := mload(Y_MPTR) + + // q_const_mptr points to Fr constants used by the VM. + // q_program_mptr points to the bytecode stream. + // Constants are stored as consecutive 32-byte Fr words. + let q_const_mptr := 0x3a60 + // Program bytes are also stored in the VK payload, packed into + // 32-byte words by PackedProgramCodec. + let q_program_mptr := 0x50a0 + // Running Horner accumulator for fully evaluated identities. + // After all identities, this is nu_y(x) for the `None` + // identity group. + // Initialize A = 0 before scanning the identity stream. + mstore(0xb280, 0) + // Simple selectors are grouped into separate linearization + // buckets. They start at zero for every proof. + // q_sel_zero_off walks selector bucket byte offsets. + for { let q_sel_zero_off := 0 } lt(q_sel_zero_off, 0x0140) { q_sel_zero_off := add(q_sel_zero_off, 0x20) } { + // B_s = 0 for each simple selector bucket. + mstore(add(SELECTOR_ACC_MPTR, q_sel_zero_off), 0) + } + // Codegen knows the selector identity positions. Precompute + // the y^k powers needed for selector gap and tail updates, + // avoiding a runtime y^-1 modexp and per-identity selector + // scale maintenance. + { + // q_y_power holds y^i at the current loop index. + let q_y_power := 1 + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0xb2c0, 1) + // Start at i=1 because y^0 = 1 is written above. + for { let q_y_power_i := 1 } lt(q_y_power_i, 49) { q_y_power_i := add(q_y_power_i, 1) } { + // Advance from y^(i-1) to y^i modulo Fr. + q_y_power := mulmod(q_y_power, y, r) + // Store y^i at selector_power_mptr + 32*i. + mstore(add(0xb2c0, shl(5, q_y_power_i)), q_y_power) + } + } + + // Direct inline prefix. These identities are generated as Yul + // before entering the VM. They use the same fold snippets as + // VM/native identities, so they occupy the same y-batch order. + { + let var0 := 0x1 + let f_3 := mload(0x9ac0) + let f_4 := mload(0x99c0) + let a_0 := mload(0x94a0) + let var1 := mulmod(f_4, a_0, r) + let var2 := addmod(f_3, var1, r) + let f_5 := mload(0x99e0) + let a_1 := mload(0x94c0) + let var3 := mulmod(f_5, a_1, r) + let var4 := addmod(var2, var3, r) + let f_6 := mload(0x9a00) + let a_2 := mload(0x94e0) + let var5 := mulmod(f_6, a_2, r) + let var6 := addmod(var4, var5, r) + let f_7 := mload(0x9a20) + let a_3 := mload(0x9500) + let var7 := mulmod(f_7, a_3, r) + let var8 := addmod(var6, var7, r) + let f_8 := mload(0x9a40) + let a_4 := mload(0x9520) + let var9 := mulmod(f_8, a_4, r) + let var10 := addmod(var8, var9, r) + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var11 := mulmod(f_0, a_0_next_1, r) + let var12 := addmod(var10, var11, r) + let f_1 := mload(0x9a80) + let var13 := mulmod(f_1, a_0, r) + let var14 := mulmod(var13, a_1, r) + let var15 := addmod(var12, var14, r) + let f_2 := mload(0x9aa0) + let var16 := mulmod(f_2, a_0, r) + let var17 := mulmod(var16, a_2, r) + let var18 := addmod(var15, var17, r) + let var19 := mulmod(var0, var18, r) + mstore(0xb8e0, var19) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x94c0) + let a_2 := mload(0x94e0) + let var1 := addmod(a_1, a_2, r) + let a_3 := mload(0x9500) + let var2 := addmod(0, sub(r, a_3), r) + let var3 := addmod(var1, var2, r) + let a_4 := mload(0x9520) + let var4 := addmod(0, sub(r, a_4), r) + let var5 := addmod(var3, var4, r) + let var6 := mulmod(var0, var5, r) + mstore(0xb8e0, var6) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let f_4 := mload(0x99c0) + let var1 := addmod(a_0, f_4, r) + let a_0_next_1 := mload(0x9540) + let var2 := addmod(0, sub(r, a_0_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xb8e0, var4) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x94c0) + let f_5 := mload(0x99e0) + let var1 := addmod(a_1, f_5, r) + let a_1_next_1 := mload(0x9560) + let var2 := addmod(0, sub(r, a_1_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xb8e0, var4) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + + // VM registers: + // q_pc current bytecode pointer + // q_end end of bytecode stream + // q_sp memory stack pointer for non-top stack values + // q_top cached top-of-stack value + // q_has_top whether q_top currently holds a stack value + // + // The cached top reduces memory traffic in the interpreter. + // q_sp's registered range must cover the interpreted operand + // stack plus any native callback scratch that reuses this base + // pointer. In particular, the native permutation callback + // writes a structured scratch table at program.stack_mptr. + // q_pc starts at the first encoded instruction. + let q_pc := q_program_mptr + // q_end is an exclusive byte pointer for the VM loop. + let q_end := add(q_program_mptr, 0x11cf) + // q_sp starts at the first free stack word. + let q_sp := 0xb8e0 + // q_top is meaningless until q_has_top is set. + let q_top := 0 + // q_has_top = 0 means the VM stack is empty. + let q_has_top := 0 + + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity + // 0x21 modarith7 + // + // The default IVC verifier uses one physical encoding for the + // logical VM: compact byte-oriented opcodes with variable-width + // operands, dynamic runs, and limb-aware cases. + + // Byte-oriented encoding: opcodes are one byte followed by + // variable-width operand bytes. + for { } lt(q_pc, q_end) { } { + // The bytecode table is byte-addressed, but EVM memory + // loads whole words. `byte(0, mload(q_pc))` extracts the + // opcode at the current byte cursor; each case advances + // q_pc by exactly its operand width. + let q_op := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + + switch q_op + // VM 0x05 PUSH_MEM_U16 (bytes): next two bytes are a short memory pointer. + case 0x05 { + // Operand layout: u16 absolute memory pointer. The + // memory planner keeps the hot quotient frame below + // 64 KiB when this compact form is emitted. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + // VM 0x06 ADD: pop one spilled stack word and add it to q_top. + case 0x06 { + // The safety validator guarantees a spilled operand + // exists before ADD. q_top is the right operand. + if eq(q_sp, 0xb8e0) { q_program_fail() } + q_sp := sub(q_sp, 0x20) + q_top := addmod(mload(q_sp), q_top, r) + } + // VM 0x08 NEG: replace q_top with its Fr negation. + case 0x08 { + // addmod(0, r - x, r) maps zero back to zero and every + // nonzero scalar to its canonical additive inverse. + q_top := addmod(0, sub(r, q_top), r) + } + // VM 0x0d MUL_CONST_U8: multiply q_top by a small constant-table slot. + case 0x0d { + // One-byte constant-index multiply, used by short + // affine chains after an initial PUSH. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + // VM 0x10 ADD_MEM_U16: add a short memory load into q_top. + case 0x10 { + // Operand layout: u16 pointer. The pointed word is an + // already range-checked Fr scalar in verifier memory. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_top := addmod(q_top, mload(q_ptr), r) + } + // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. + case 0x11 { + // In-place multiply by a planned memory word. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_top := mulmod(q_top, mload(q_ptr), r) + } + // Limb-aware opcodes are opt-in compact forms for + // structurally recognized non-SHA foreign-field shapes. + // Coefficients are indexes into q_const_mptr, which is + // generated from VK/program data, never from proof + // calldata. + // + // Rust source shape: + // proofs/src/plonk/mod.rs::partially_evaluate_identities + // circuits/src/field/foreign/util.rs::{sum_exprs,pair_wise_prod} + // circuits/src/field/foreign/params.rs::{base_powers,double_base_powers} + // + // "Foreign field" means the circuit represents elements + // modulo another modulus m as 7 limbs in base + // 2^LOG2_BASE. The verifier does not switch fields; it + // evaluates the lowered identity over BLS12-381 Fr, using + // Fr coefficients equal to base^i mod m or base^(i+j) mod m. + // VM 0x21 MODARITH7: byte-only fused affine 7-limb foreign-field/ECC identity. + case 0x21 { + // MODARITH7: + // maybe_cond * ( + // c + // + sum LIN7 blocks + // + sum BILIN7_ROW blocks + // + sum BILIN7_PAIRWISE blocks + // + sum coeff[k] * mload(ptr[k]) + // + sum coeff[k] * mload(lhs[k]) * mload(rhs[k]) + // ) + // It is a dispatch/operand-load optimization only; + // all coefficients still come from the generated + // quotient constant table. + // + // Flags: + // bit 0: multiply the final affine sum by a memory + // condition word. + // bit 1: seed q_acc from a constant-table word + // before reading the counted term blocks. + let q_flags := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + let q_cond_ptr := 0 + if and(q_flags, 0x01) { + // Optional condition pointer. When present, the + // whole identity is gated by mload(q_cond_ptr). + q_cond_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_cond_ptr, 0x3680), 0x6aa0) { q_program_fail() } + } + + let q_acc := 0 + if and(q_flags, 0x02) { + // Optional constant seed for affine identities + // with a standalone constant term. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_acc := mload(add(q_const_mptr, shl(5, qconst))) + } + + // Five one-byte counters describe the blocks that + // follow. Each block has a fixed-width internal layout, + // so q_pc can advance without per-term tags. + let q_counts_word := mload(q_pc) + let q_lin_count := byte(0, q_counts_word) + let q_row_count := byte(1, q_counts_word) + let q_pairwise_count := byte(2, q_counts_word) + let q_mem_count := byte(3, q_counts_word) + let q_product_count := byte(4, q_counts_word) + q_pc := add(q_pc, 5) + + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + + // LIN7 blocks: q_acc += sum_i c_i * limb_i. + for { let q_lin_block := 0 } lt(q_lin_block, q_lin_count) { q_lin_block := add(q_lin_block, 1) } { + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + } + + // BILIN7_ROW blocks: q_acc += lhs * sum_i c_i * rhs_i. + for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { + let q_lhs := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } + let q_lhs_value := mload(q_lhs) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_rhs := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + + // BILIN7_PAIRWISE blocks: q_acc += weighted 7-by-7 + // product convolution. + for { let q_pair_block := 0 } lt(q_pair_block, q_pairwise_count) { q_pair_block := add(q_pair_block, 1) } { + let q_pair_word := mload(q_pc) + let q_lhs_base := shr(240, q_pair_word) + let q_rhs_base := and(shr(224, q_pair_word), 0xffff) + q_pc := add(q_pc, 0x04) + if gt(sub(q_lhs_base, 0x3680), 0x69e0) { q_program_fail() } + if gt(sub(q_rhs_base, 0x3680), 0x69e0) { q_program_fail() } + let q_coeff_pc := q_pc + q_pc := add(q_pc, 13) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_lhs_value := mload(add(q_lhs_base, shl(5, q_i))) + for { let q_j := 0 } lt(q_j, 7) { q_j := add(q_j, 1) } { + let qconst := byte(0, mload(add(q_coeff_pc, add(q_i, q_j)))) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(add(q_rhs_base, shl(5, q_j))), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + } + + // Extra linear memory terms outside the 7-limb shapes. + for { let q_mem_block := 0 } lt(q_mem_block, q_mem_count) { q_mem_block := add(q_mem_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + + // Extra binary product terms outside the 7-limb shapes. + for { let q_product_block := 0 } lt(q_product_block, q_product_count) { q_product_block := add(q_product_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_lhs := and(shr(232, q_word), 0xffff) + let q_rhs := and(shr(216, q_word), 0xffff) + q_pc := add(q_pc, 5) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + + if and(q_flags, 0x01) { + // Apply the optional gate condition last so every + // subterm shares the same selector/condition. + q_acc := mulmod(mload(q_cond_ptr), q_acc, r) + } + // MODARITH7 pushes its fused identity value. + q_top := q_acc + q_has_top := 1 + } + // Native permutation callback. It evaluates the + // permutation identities from permutation.rs at this exact + // VM position, preserving the Rust identity order while + // avoiding a large interpreted product loop. + // VM 0x19 NATIVE_PERMUTATION: marker for the generated permutation callback. + case 0x19 { + // Native callbacks are identity-boundary opcodes. They + // must not inherit any partially evaluated VM stack + // state from the previous expression. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // its scratch-table base, not as a conventional VM + // stack. The Rust memory planner must reserve enough + // words for structured_permutation_scratch_words(meta) + // whenever this opcode can appear. + q_sp := 0xb8e0 + // The generated lines below call the same fold snippets + // used by interpreted expressions, so trace IDs and + // y-batch positions remain contiguous. + { + let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 + let q_perm_vals := 0xb8e0 + let q_perm_sigmas := 0xbb20 + let q_perm_z_cur := 0xbd60 + let q_perm_z_next := 0xbe20 + let q_perm_z_last := 0xbee0 + let q_perm_delta_base_ptr := 0xbf80 + let q_perm_num_cols := 18 + let q_perm_num_sets := 6 + let q_perm_chunk_len := 3 + let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 + mstore(add(q_perm_vals, 0x0), mload(0x99a0)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x94a0, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0xc0), mload(0x9480)) + mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 9) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x100), q_perm_val_load_dst_off), mload(add(0x95a0, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0x220), mload(0x9980)) + { + for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 18) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { + let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) + let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x9bc0, q_perm_sigma_load_src_off))) + } + } + { + for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 6) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { + let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) + let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x9e00, q_perm_z_cur_load_src_off))) + } + } + { + for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 6) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { + let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) + let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x9e20, q_perm_z_next_load_src_off))) + } + } + { + for { let q_perm_z_last_load_i := 0 } lt(q_perm_z_last_load_i, 5) { q_perm_z_last_load_i := add(q_perm_z_last_load_i, 1) } { + let q_perm_z_last_load_dst_off := shl(5, q_perm_z_last_load_i) + let q_perm_z_last_load_src_off := mul(q_perm_z_last_load_i, 0x60) + mstore(add(add(q_perm_z_last, 0x0), q_perm_z_last_load_dst_off), mload(add(0x9e40, q_perm_z_last_load_src_off))) + } + } + let q_perm_eval := 0 + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + let q_perm_zn := mload(add(q_perm_z_cur, 0xa0)) + q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + for { let q_perm_i := 1 } lt(q_perm_i, 6) { q_perm_i := add(q_perm_i, 1) } { + let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) + let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + } + mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) + for { let q_perm_set := 0 } lt(q_perm_set, 6) { q_perm_set := add(q_perm_set, 1) } { + let q_perm_start := mul(q_perm_set, q_perm_chunk_len) + let q_perm_end := add(q_perm_start, q_perm_chunk_len) + if gt(q_perm_end, q_perm_num_cols) { q_perm_end := q_perm_num_cols } + let q_perm_left := mload(add(q_perm_z_next, shl(5, q_perm_set))) + let q_perm_right := mload(add(q_perm_z_cur, shl(5, q_perm_set))) + let q_perm_delta_pow := mload(q_perm_delta_base_ptr) + for { let q_perm_j := q_perm_start } lt(q_perm_j, q_perm_end) { q_perm_j := add(q_perm_j, 1) } { + let q_perm_off := shl(5, q_perm_j) + let q_perm_v := mload(add(q_perm_vals, q_perm_off)) + let q_perm_s := mload(add(q_perm_sigmas, q_perm_off)) + q_perm_left := mulmod(q_perm_left, addmod(addmod(q_perm_v, mulmod(mload(BETA_MPTR), q_perm_s, r), r), mload(GAMMA_MPTR), r), r) + q_perm_right := mulmod(q_perm_right, addmod(addmod(q_perm_v, q_perm_delta_pow, r), mload(GAMMA_MPTR), r), r) + q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) + } + q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) + } + } + } + // Native lookup callback. This whole-family opcode + // evaluates the LogUp boundary, helper-chunk, and + // accumulator identities at this VM position, preserving + // the Rust y-batch order while avoiding many interpreted + // product-loop opcodes. + // VM 0x1f NATIVE_LOOKUP: marker for the generated LogUp lookup callback. + case 0x1f { + // Reset VM stack state before entering structured + // lookup Yul. Lookup callbacks own their scratch + // layout and perform all needed folds internally. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // f+beta/prefix/suffix scratch rather than as a + // conventional VM stack. The Rust memory planner must + // reserve structured_lookup_scratch_words(meta). + q_sp := 0xb8e0 + // Generated LogUp code follows the same y-batch order + // as the Rust identity stream. + { + let q_lookup_f := 0xb8e0 + let q_lookup_prefix := 0xb960 + let q_lookup_suffix := 0xb9e0 + let q_lookup_l0 := mload(L_0_MPTR) + let q_lookup_llast := mload(L_LAST_MPTR) + let q_lookup_lblind := mload(L_BLIND_MPTR) + let q_lookup_lsum := addmod(q_lookup_l0, q_lookup_llast, r) + let q_lookup_active := addmod(1, sub(r, addmod(q_lookup_llast, q_lookup_lblind, r)), r) + let q_lookup_beta := mload(BETA_MPTR) + let q_lookup_theta := mload(THETA_MPTR) + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa060), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let f_10 := mload(0x9ae0) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) + let var1 := mulmod(var0, q_lookup_theta, r) + for { let q_lookup_shared_i := 0 } lt(q_lookup_shared_i, 4) { q_lookup_shared_i := add(q_lookup_shared_i, 1) } { + let q_lookup_shared_off := shl(5, q_lookup_shared_i) + let q_lookup_shared_tail := mload(add(0x94c0, q_lookup_shared_off)) + let q_lookup_shared_compressed := addmod(var1, q_lookup_shared_tail, r) + mstore(add(q_lookup_f, q_lookup_shared_off), addmod(q_lookup_shared_compressed, q_lookup_beta, r)) + } + let q_lookup_product := 1 + for { let q_lookup_prod_i := 0 } lt(q_lookup_prod_i, 4) { q_lookup_prod_i := add(q_lookup_prod_i, 1) } { + q_lookup_product := mulmod(q_lookup_product, mload(add(q_lookup_f, shl(5, q_lookup_prod_i))), r) + } + mstore(q_lookup_prefix, 1) + for { let q_lookup_pref_i := 1 } lt(q_lookup_pref_i, 4) { q_lookup_pref_i := add(q_lookup_pref_i, 1) } { + let q_lookup_pref_prev := sub(q_lookup_pref_i, 1) + mstore(add(q_lookup_prefix, shl(5, q_lookup_pref_i)), mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_pref_prev))), mload(add(q_lookup_f, shl(5, q_lookup_pref_prev))), r)) + } + mstore(add(q_lookup_suffix, 0x60), 1) + for { let q_lookup_suf_i := sub(4, 1) } gt(q_lookup_suf_i, 0) { q_lookup_suf_i := sub(q_lookup_suf_i, 1) } { + let q_lookup_suf_prev := sub(q_lookup_suf_i, 1) + mstore(add(q_lookup_suffix, shl(5, q_lookup_suf_prev)), mulmod(mload(add(q_lookup_suffix, shl(5, q_lookup_suf_i))), mload(add(q_lookup_f, shl(5, q_lookup_suf_i))), r)) + } + let q_lookup_sum := 0 + for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 4) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { + q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) + } + let q_lookup_eval := addmod(mulmod(mload(0xa040), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0xa040) + let f_17 := mload(0x9b60) + let f_11 := mload(0x9b00) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) + let f_12 := mload(0x9b20) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) + let q_lookup_s_sum_h := mulmod(f_17, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0xa080), sub(r, addmod(mload(0xa060), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa020), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + } + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa0e0), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let a_14 := mload(0x9980) + let var0 := addmod(mulmod(0, q_lookup_theta, r), a_14, r) + let a_0 := mload(0x94a0) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_0, r) + let a_1 := mload(0x94c0) + let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_1, r) + let a_2 := mload(0x94e0) + let var3 := addmod(mulmod(var2, q_lookup_theta, r), a_2, r) + let a_3 := mload(0x9500) + let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_3, r) + let a_4 := mload(0x9520) + let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_4, r) + let a_5 := mload(0x95a0) + let var6 := addmod(mulmod(var5, q_lookup_theta, r), a_5, r) + let a_6 := mload(0x95c0) + let var7 := addmod(mulmod(var6, q_lookup_theta, r), a_6, r) + let a_7 := mload(0x95e0) + let var8 := addmod(mulmod(var7, q_lookup_theta, r), a_7, r) + let a_8 := mload(0x9600) + let var9 := addmod(mulmod(var8, q_lookup_theta, r), a_8, r) + let a_9 := mload(0x9620) + let var10 := addmod(mulmod(var9, q_lookup_theta, r), a_9, r) + let a_10 := mload(0x9640) + let var11 := addmod(mulmod(var10, q_lookup_theta, r), a_10, r) + let a_11 := mload(0x9660) + let var12 := addmod(mulmod(var11, q_lookup_theta, r), a_11, r) + let a_12 := mload(0x9680) + let var13 := addmod(mulmod(var12, q_lookup_theta, r), a_12, r) + let a_13 := mload(0x96a0) + let var14 := addmod(mulmod(var13, q_lookup_theta, r), a_13, r) + let f_13 := mload(0x9b40) + let var15 := addmod(mulmod(var14, q_lookup_theta, r), f_13, r) + let q_lookup_eval := addmod(mulmod(mload(0xa0c0), addmod(var15, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0xa0c0) + let var0 := 0x1 + let f_26 := mload(0x9ba0) + let var1 := addmod(0, sub(r, f_26), r) + let var2 := addmod(var0, var1, r) + let a_14 := mload(0x9980) + let var3 := mulmod(var2, a_14, r) + let var4 := addmod(mulmod(0, q_lookup_theta, r), var3, r) + let a_0 := mload(0x94a0) + let var5 := mulmod(var2, a_0, r) + let var6 := addmod(mulmod(var4, q_lookup_theta, r), var5, r) + let a_1 := mload(0x94c0) + let var7 := mulmod(var2, a_1, r) + let var8 := addmod(mulmod(var6, q_lookup_theta, r), var7, r) + let a_2 := mload(0x94e0) + let var9 := mulmod(var2, a_2, r) + let var10 := addmod(mulmod(var8, q_lookup_theta, r), var9, r) + let a_3 := mload(0x9500) + let var11 := mulmod(var2, a_3, r) + let var12 := addmod(mulmod(var10, q_lookup_theta, r), var11, r) + let a_4 := mload(0x9520) + let var13 := mulmod(var2, a_4, r) + let var14 := addmod(mulmod(var12, q_lookup_theta, r), var13, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var2, a_5, r) + let var16 := addmod(mulmod(var14, q_lookup_theta, r), var15, r) + let a_6 := mload(0x95c0) + let var17 := mulmod(var2, a_6, r) + let var18 := addmod(mulmod(var16, q_lookup_theta, r), var17, r) + let a_7 := mload(0x95e0) + let var19 := mulmod(var2, a_7, r) + let var20 := addmod(mulmod(var18, q_lookup_theta, r), var19, r) + let a_8 := mload(0x9600) + let var21 := mulmod(var2, a_8, r) + let var22 := addmod(mulmod(var20, q_lookup_theta, r), var21, r) + let a_9 := mload(0x9620) + let var23 := mulmod(var2, a_9, r) + let var24 := addmod(mulmod(var22, q_lookup_theta, r), var23, r) + let a_10 := mload(0x9640) + let var25 := mulmod(var2, a_10, r) + let var26 := addmod(mulmod(var24, q_lookup_theta, r), var25, r) + let a_11 := mload(0x9660) + let var27 := mulmod(var2, a_11, r) + let var28 := addmod(mulmod(var26, q_lookup_theta, r), var27, r) + let a_12 := mload(0x9680) + let var29 := mulmod(var2, a_12, r) + let var30 := addmod(mulmod(var28, q_lookup_theta, r), var29, r) + let a_13 := mload(0x96a0) + let var31 := mulmod(var2, a_13, r) + let var32 := addmod(mulmod(var30, q_lookup_theta, r), var31, r) + let f_13 := mload(0x9b40) + let var33 := mulmod(var2, f_13, r) + let var34 := addmod(mulmod(var32, q_lookup_theta, r), var33, r) + let q_lookup_s_sum_h := mulmod(var0, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0xa100), sub(r, addmod(mload(0xa0e0), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var34, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa0a0), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + } + } + } + // Native callbacks are generated only for the heaviest + // recognized Midfall gate identities. All other gate and + // non-native identity arithmetic remains in + // the compact q_program VM above, preserving the Rust + // `partially_evaluate_identities` order. + // VM 0x1b NATIVE_IDENTITY: marker for generated heavy-gate callbacks. + case 0x1b { + // Operand layout: u16 native callback index. The + // manifest validates that callback indexes appear in + // generated order and target existing switch cases. + let q_native_idx := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + // Heavy identities are whole expressions, so clear the + // interpreter stack before dispatching. + q_top := 0 + q_has_top := 0 + q_sp := 0xb8e0 + // Native identity sub-cases are generated from selected heavy gate identities. + switch q_native_idx + case 0 { + { + let var0 := 0x1 + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var1 := addmod(0, sub(r, a_0_next_1), r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace + let a_0 := mload(0x94a0) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x9500) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 + let a_1 := mload(0x94c0) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x9520) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_2 := mload(0x94e0) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xb8e0, var18) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 1 { + { + let var0 := 0x1 + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) + let var1 := addmod(0, sub(r, a_1_next_1), r) + let var2 := addmod(f_1, var1, r) + let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 + let a_0 := mload(0x94a0) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x9500) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 + let a_1 := mload(0x94c0) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x9520) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let a_2 := mload(0x94e0) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xb8e0, var18) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 2 { + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let a_0_next_1 := mload(0x9540) + let var1 := mulmod(a_0, a_0_next_1, r) + let var2 := 0x100000000000000 + let a_1_next_1 := mload(0x9560) + let var3 := mulmod(a_0, a_1_next_1, r) + let var4 := mulmod(var2, var3, r) + let var5 := addmod(var1, var4, r) + let var6 := 0x10000000000000000000000000000 + let a_2_next_1 := mload(0x9580) + let var7 := mulmod(a_0, a_2_next_1, r) + let var8 := mulmod(var6, var7, r) + let var9 := addmod(var5, var8, r) + let a_1 := mload(0x94c0) + let var10 := mulmod(a_1, a_0_next_1, r) + let var11 := mulmod(var2, var10, r) + let var12 := addmod(var9, var11, r) + let var13 := mulmod(a_1, a_1_next_1, r) + let var14 := mulmod(var6, var13, r) + let var15 := addmod(var12, var14, r) + let var16 := 0x3212e00cde6d2002b119d800000347fcb8 + let a_6_next_1 := mload(0x9720) + let var17 := mulmod(a_1, a_6_next_1, r) + let var18 := mulmod(var16, var17, r) + let var19 := addmod(var15, var18, r) + let a_2 := mload(0x94e0) + let var20 := mulmod(a_2, a_0_next_1, r) + let var21 := mulmod(var6, var20, r) + let var22 := addmod(var19, var21, r) + let a_5_next_1 := mload(0x9700) + let var23 := mulmod(a_2, a_5_next_1, r) + let var24 := mulmod(var16, var23, r) + let var25 := addmod(var22, var24, r) + let var26 := 0x297784894e27525bc342b7fde37dba9366 + let var27 := mulmod(a_2, a_6_next_1, r) + let var28 := mulmod(var26, var27, r) + let var29 := addmod(var25, var28, r) + let a_3 := mload(0x9500) + let a_4_next_1 := mload(0x96e0) + let var30 := mulmod(a_3, a_4_next_1, r) + let var31 := mulmod(var16, var30, r) + let var32 := addmod(var29, var31, r) + let var33 := mulmod(a_3, a_5_next_1, r) + let var34 := mulmod(var26, var33, r) + let var35 := addmod(var32, var34, r) + let var36 := 0x340f2ebe380a0f5eff4360543988a61dc2 + let var37 := mulmod(a_3, a_6_next_1, r) + let var38 := mulmod(var36, var37, r) + let var39 := addmod(var35, var38, r) + let a_4 := mload(0x9520) + let a_3_next_1 := mload(0x96c0) + let var40 := mulmod(a_4, a_3_next_1, r) + let var41 := mulmod(var16, var40, r) + let var42 := addmod(var39, var41, r) + let var43 := mulmod(a_4, a_4_next_1, r) + let var44 := mulmod(var26, var43, r) + let var45 := addmod(var42, var44, r) + let var46 := mulmod(a_4, a_5_next_1, r) + let var47 := mulmod(var36, var46, r) + let var48 := addmod(var45, var47, r) + let var49 := 0x13af65741744bd7bb2c6872df2b800320 + let var50 := mulmod(a_4, a_6_next_1, r) + let var51 := mulmod(var49, var50, r) + let var52 := addmod(var48, var51, r) + let a_5 := mload(0x95a0) + let var53 := mulmod(a_5, a_2_next_1, r) + let var54 := mulmod(var16, var53, r) + let var55 := addmod(var52, var54, r) + let var56 := mulmod(a_5, a_3_next_1, r) + let var57 := mulmod(var26, var56, r) + let var58 := addmod(var55, var57, r) + let var59 := mulmod(a_5, a_4_next_1, r) + let var60 := mulmod(var36, var59, r) + let var61 := addmod(var58, var60, r) + let var62 := mulmod(a_5, a_5_next_1, r) + let var63 := mulmod(var49, var62, r) + let var64 := addmod(var61, var63, r) + let var65 := 0x2cb9b546d20373eaf85e8f53db883cb548 + let var66 := mulmod(a_5, a_6_next_1, r) + let var67 := mulmod(var65, var66, r) + let var68 := addmod(var64, var67, r) + let a_6 := mload(0x95c0) + let var69 := mulmod(a_6, a_1_next_1, r) + let var70 := mulmod(var16, var69, r) + let var71 := addmod(var68, var70, r) + let var72 := mulmod(a_6, a_2_next_1, r) + let var73 := mulmod(var26, var72, r) + let var74 := addmod(var71, var73, r) + let var75 := mulmod(a_6, a_3_next_1, r) + let var76 := mulmod(var36, var75, r) + let var77 := addmod(var74, var76, r) + let var78 := mulmod(a_6, a_4_next_1, r) + let var79 := mulmod(var49, var78, r) + let var80 := addmod(var77, var79, r) + let var81 := mulmod(a_6, a_5_next_1, r) + let var82 := mulmod(var65, var81, r) + let var83 := addmod(var80, var82, r) + let var84 := 0xc8557e86f90d0d89eed6eb5349a0f8820 + let var85 := mulmod(a_6, a_6_next_1, r) + let var86 := mulmod(var84, var85, r) + let var87 := addmod(var83, var86, r) + let var88 := mulmod(var2, a_1, r) + let var89 := addmod(a_0, var88, r) + let var90 := mulmod(var6, a_2, r) + let var91 := addmod(var89, var90, r) + let var92 := addmod(var87, var91, r) + let var93 := mulmod(var2, a_1_next_1, r) + let var94 := addmod(a_0_next_1, var93, r) + let var95 := mulmod(var6, a_2_next_1, r) + let var96 := addmod(var94, var95, r) + let var97 := addmod(var92, var96, r) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) + let var98 := mulmod(var2, a_8, r) + let var99 := addmod(a_7, var98, r) + let a_9 := mload(0x9620) + let var100 := mulmod(var6, a_9, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(0, sub(r, var101), r) + let var103 := addmod(var97, var102, r) + let a_7_next_1 := mload(0x9740) + let var104 := 0x241eabfffeb153ffffb9feffffffffaaab + let var105 := mulmod(a_7_next_1, var104, r) + let var106 := addmod(0, sub(r, var105), r) + let var107 := addmod(var103, var106, r) + let var108 := addmod(0, sub(r, var16), r) + let var109 := addmod(var107, var108, r) + let a_8_next_1 := mload(0x9760) + let var110 := 0x73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb20000001 + let var111 := addmod(a_8_next_1, var110, r) + let var112 := 0x4000000000000000000000000000000000 + let var113 := mulmod(var111, var112, r) + let var114 := addmod(0, sub(r, var113), r) + let var115 := addmod(var109, var114, r) + let var116 := mulmod(var0, var115, r) + mstore(0xb8e0, var116) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x80) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 3 { + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let var1 := 0x10000000000000000000000000000 + let var2 := addmod(a_0, var1, r) + let var3 := 0x100000000000000 + let a_1 := mload(0x94c0) + let var4 := addmod(a_1, var1, r) + let var5 := mulmod(var3, var4, r) + let var6 := addmod(var2, var5, r) + let a_2 := mload(0x94e0) + let var7 := addmod(a_2, var1, r) + let var8 := mulmod(var1, var7, r) + let var9 := addmod(var6, var8, r) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) + let var10 := mulmod(var3, a_8, r) + let var11 := addmod(a_7, var10, r) + let a_9 := mload(0x9620) + let var12 := mulmod(var1, a_9, r) + let var13 := addmod(var11, var12, r) + let var14 := addmod(0, sub(r, var13), r) + let var15 := addmod(var9, var14, r) + let var16 := addmod(0, sub(r, var1), r) + let var17 := addmod(var15, var16, r) + let a_7_next_1 := mload(0x9740) + let var18 := 0x241eabfffeb153ffffb9feffffffffaaab + let var19 := mulmod(a_7_next_1, var18, r) + let var20 := addmod(0, sub(r, var19), r) + let var21 := addmod(var17, var20, r) + let var22 := 0xd9d44a30b019261257667fde3844a8cd6 + let var23 := addmod(0, sub(r, var22), r) + let var24 := addmod(var21, var23, r) + let a_8_next_1 := mload(0x9760) + let var25 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab00002 + let var26 := addmod(a_8_next_1, var25, r) + let var27 := 0x4000000000000000000000000000000000 + let var28 := mulmod(var26, var27, r) + let var29 := addmod(0, sub(r, var28), r) + let var30 := addmod(var24, var29, r) + let var31 := mulmod(var0, var30, r) + mstore(0xb8e0, var31) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xa0) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + default { q_program_fail() } + } + // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. + case 0x0b { + // Operand layout packed into three bytes: + // high byte: selector bucket index; + // low u16 : y-power gap since this selector's + // previous contribution. + let q_selector_payload := shr(232, mload(q_pc)) + q_pc := add(q_pc, 3) + let q_sel_idx := shr(16, q_selector_payload) + let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 10)) { q_program_fail() } + if gt(q_sel_gap, 0x30) { q_program_fail() } + let q_eval := q_top + q_has_top := 0 + // Simple-selector identity: keep the same y-batch + // position as main identities, then advance only this + // selector bucket by its codegen-known gap. + // + // The global fully-evaluated accumulator is still + // multiplied by y so later main identities land at the + // same y powers as Rust's reverse fold. + mstore(0xb280, mulmod(mload(0xb280), y, r)) + let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) + let q_sel_acc := mload(q_target_ptr) + if q_sel_gap { + // Selector buckets are sparse in the global + // identity stream. Precomputed y^gap advances only + // this selector's local accumulator. + q_sel_acc := mulmod(q_sel_acc, mload(add(0xb2c0, shl(5, q_sel_gap))), r) + } + mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) + } + // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. + default { + q_program_fail() + } + } + // The VK-pinned bytecode must end exactly at q_end and every + // identity must have been consumed by a fold/native callback. + // This catches malformed generator output whose final opcode + // over-reads operands or leaves a partial expression live. + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0xb8e0)) { q_program_fail() } + + // Structured post-VM suffix. The current default uses this for + // regular trash constraints: it is smaller than fully unrolled + // Yul and cheaper than interpreting every trash operation. + // + // These generated blocks run after q_pc reaches q_end, but + // they still participate in the same identity order and write + // into the same numerator / selector accumulators. + { + let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) + { + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 + let var1 := mulmod(a_0_next_1, var0, r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 + let a_0 := mload(0x94a0) + let var4 := mulmod(var3, a_0, r) + let var5 := addmod(var2, var4, r) + let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f + let a_1 := mload(0x94c0) + let var7 := mulmod(var6, a_1, r) + let var8 := addmod(var5, var7, r) + let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 + let a_2 := mload(0x94e0) + let var10 := q_pow5(a_2) + let var11 := mulmod(var9, var10, r) + let var12 := addmod(var8, var11, r) + let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 + let a_3 := mload(0x9500) + let var14 := q_pow5(a_3) + let var15 := mulmod(var13, var14, r) + let var16 := addmod(var12, var15, r) + let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 + let a_4 := mload(0x9520) + let var18 := q_pow5(a_4) + let var19 := mulmod(var17, var18, r) + let var20 := addmod(var16, var19, r) + let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e + let a_5 := mload(0x95a0) + let var22 := q_pow5(a_5) + let var23 := mulmod(var21, var22, r) + let var24 := addmod(var20, var23, r) + let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 + let a_6 := mload(0x95c0) + let var26 := q_pow5(a_6) + let var27 := mulmod(var25, var26, r) + let var28 := addmod(var24, var27, r) + let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_7 := mload(0x95e0) + let var30 := q_pow5(a_7) + let var31 := mulmod(var29, var30, r) + let var32 := addmod(var28, var31, r) + let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) + let var34 := mulmod(a_1_next_1, var0, r) + let var35 := addmod(f_1, var34, r) + let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d + let var37 := mulmod(var36, a_0, r) + let var38 := addmod(var35, var37, r) + let var39 := 0x4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028 + let var40 := mulmod(var39, a_1, r) + let var41 := addmod(var38, var40, r) + let var42 := 0x26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10 + let var43 := mulmod(var42, var10, r) + let var44 := addmod(var41, var43, r) + let var45 := 0x31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d25 + let var46 := mulmod(var45, var14, r) + let var47 := addmod(var44, var46, r) + let var48 := 0x275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85 + let var49 := mulmod(var48, var18, r) + let var50 := addmod(var47, var49, r) + let var51 := 0x5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f701 + let var52 := mulmod(var51, var22, r) + let var53 := addmod(var50, var52, r) + let var54 := 0x301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd + let var55 := mulmod(var54, var26, r) + let var56 := addmod(var53, var55, r) + let var57 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let var58 := mulmod(var57, var30, r) + let var59 := addmod(var56, var58, r) + let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) + let f_2 := mload(0x9aa0) + let var61 := mulmod(a_3, var0, r) + let var62 := addmod(f_2, var61, r) + let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 + let var64 := mulmod(var63, a_0, r) + let var65 := addmod(var62, var64, r) + let var66 := 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a + let var67 := mulmod(var66, a_1, r) + let var68 := addmod(var65, var67, r) + let var69 := 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db + let var70 := mulmod(var69, var10, r) + let var71 := addmod(var68, var70, r) + let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) + let f_3 := mload(0x9ac0) + let var73 := mulmod(a_4, var0, r) + let var74 := addmod(f_3, var73, r) + let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 + let var76 := mulmod(var75, a_0, r) + let var77 := addmod(var74, var76, r) + let var78 := 0x26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191 + let var79 := mulmod(var78, a_1, r) + let var80 := addmod(var77, var79, r) + let var81 := 0x4382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 + let var82 := mulmod(var81, var10, r) + let var83 := addmod(var80, var82, r) + let var84 := mulmod(var69, var14, r) + let var85 := addmod(var83, var84, r) + let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) + let f_4 := mload(0x99c0) + let var87 := mulmod(a_5, var0, r) + let var88 := addmod(f_4, var87, r) + let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b + let var90 := mulmod(var89, a_0, r) + let var91 := addmod(var88, var90, r) + let var92 := 0x24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520 + let var93 := mulmod(var92, a_1, r) + let var94 := addmod(var91, var93, r) + let var95 := 0x4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea29 + let var96 := mulmod(var95, var10, r) + let var97 := addmod(var94, var96, r) + let var98 := mulmod(var81, var14, r) + let var99 := addmod(var97, var98, r) + let var100 := mulmod(var69, var18, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) + let f_5 := mload(0x99e0) + let var103 := mulmod(a_6, var0, r) + let var104 := addmod(f_5, var103, r) + let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 + let var106 := mulmod(var105, a_0, r) + let var107 := addmod(var104, var106, r) + let var108 := 0x23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827 + let var109 := mulmod(var108, a_1, r) + let var110 := addmod(var107, var109, r) + let var111 := 0x1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e + let var112 := mulmod(var111, var10, r) + let var113 := addmod(var110, var112, r) + let var114 := mulmod(var95, var14, r) + let var115 := addmod(var113, var114, r) + let var116 := mulmod(var81, var18, r) + let var117 := addmod(var115, var116, r) + let var118 := mulmod(var69, var22, r) + let var119 := addmod(var117, var118, r) + let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) + let f_6 := mload(0x9a00) + let var121 := mulmod(a_7, var0, r) + let var122 := addmod(f_6, var121, r) + let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a + let var124 := mulmod(var123, a_0, r) + let var125 := addmod(var122, var124, r) + let var126 := 0x27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849 + let var127 := mulmod(var126, a_1, r) + let var128 := addmod(var125, var127, r) + let var129 := 0xd94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533 + let var130 := mulmod(var129, var10, r) + let var131 := addmod(var128, var130, r) + let var132 := mulmod(var111, var14, r) + let var133 := addmod(var131, var132, r) + let var134 := mulmod(var95, var18, r) + let var135 := addmod(var133, var134, r) + let var136 := mulmod(var81, var22, r) + let var137 := addmod(var135, var136, r) + let var138 := mulmod(var69, var26, r) + let var139 := addmod(var137, var138, r) + let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) + let f_7 := mload(0x9a20) + let a_2_next_1 := mload(0x9580) + let var141 := mulmod(a_2_next_1, var0, r) + let var142 := addmod(f_7, var141, r) + let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 + let var144 := mulmod(var143, a_0, r) + let var145 := addmod(var142, var144, r) + let var146 := 0x40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261 + let var147 := mulmod(var146, a_1, r) + let var148 := addmod(var145, var147, r) + let var149 := 0x1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e + let var150 := mulmod(var149, var10, r) + let var151 := addmod(var148, var150, r) + let var152 := mulmod(var129, var14, r) + let var153 := addmod(var151, var152, r) + let var154 := mulmod(var111, var18, r) + let var155 := addmod(var153, var154, r) + let var156 := mulmod(var95, var22, r) + let var157 := addmod(var155, var156, r) + let var158 := mulmod(var81, var26, r) + let var159 := addmod(var157, var158, r) + let var160 := mulmod(var69, var30, r) + let var161 := addmod(var159, var160, r) + let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) + let f_19 := mload(0x9b80) + let q_trash_one_minus_selector := addmod(1, sub(r, f_19), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0xa120), r) + let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_trash_eval, r)) + } + } + // Finish selector buckets by applying the codegen-known tail + // from each selector's last identity to the end of the global + // y-batch. + // + // After this step, every selector bucket is aligned with the + // final global y position and can be multiplied by its fixed + // selector commitment in the linearized MSM. + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0600)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x05e0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0580)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x04c0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0460)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0400)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x03a0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0340)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x02e0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0280)), r)) + } + + // Fully evaluated identities are the constant-polynomial side + // of the linearization query. Rust subtracts that grouped + // scalar into expected_eval, so Solidity stores -nu_y(x). + let linearization_expected_eval := addmod(0, sub(r, mload(0xb280)), r) + mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) + pop(y) + } + + // Return the compact output frame. Halo2Verifier checks the magic, + // stores word 1 as the linearization expected eval, then expands + // selector buckets into the fused final PCS MSM. + mstore(QUOTIENT_OUTPUT_MPTR, QUOTIENT_MAGIC) + mstore(add(QUOTIENT_OUTPUT_MPTR, 0x20), mload(QUOTIENT_EVAL_MPTR)) + // Copy selector buckets from the generated absolute memory region + // into the compact external-call return frame. + for { let q_i := 0 } lt(q_i, 10) { q_i := add(q_i, 1) } { + mstore(add(QUOTIENT_OUTPUT_MPTR, add(0x40, shl(5, q_i))), mload(add(SELECTOR_ACC_MPTR, shl(5, q_i)))) + } + return(QUOTIENT_OUTPUT_MPTR, QUOTIENT_OUTPUT_LEN) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/Halo2Verifier.sol b/proofs/solidity-verifier/fixtures/ivc/Halo2Verifier.sol new file mode 100644 index 000000000..ece93ee6b --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/Halo2Verifier.sol @@ -0,0 +1,2593 @@ +// SPDX-License-Identifier: CC0-1.0 +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; + +/// @title Halo2 BLS12-381 KZG verifier. +/// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +/// proofs rendered by this repository's Rust generator. +/// @dev This contract ports the verifier flow from +/// `midfall/proofs/src/plonk/verifier.rs`, the Keccak transcript comments from +/// `midfall/proofs/src/transcript/implementors.rs`, and the KZG multi-open +/// comments from `midfall/proofs/src/poly/kzg/mod.rs`. +/// @dev It is not a generic verifier. The proof layout, VK payload, quotient +/// identity program, memory layout, and optional quotient evaluator are all +/// generated for one `VerifyingKey>`. +/// +/// Halo2 KZG verifier for the BLS12-381 curve, midnight-proofs flavour. +/// +/// Differences vs the original BN254 / halo2 v0.4 template: +// +/// - BLS12-381 base field Fp is 381 bits and does not fit in a uint256. +/// Each Fp coord is encoded EIP-2537 padded (16 zero bytes + 48 bytes). +/// A G1 point is 128 bytes (4 words); a G2 point is 256 bytes (8). +/// - Calldata carries G1 commitments in uncompressed EIP-2537 padded +/// form (4 words = 128 bytes per point: x_hi, x_lo, y_hi, y_lo). The +/// proof bytes produced by midnight-proofs prover are repacked off +/// chain (compressed -> uncompressed) before being passed to +/// `verifyProof`. The verifier hashes the uncompressed 128-byte form into +/// the transcript verbatim, matching `Hashable for G1Projective::to_input`; +/// see `common_uncompressed_g1`. +/// - Transcript `common` absorbs raw inputs in order. `squeeze` computes one +/// Keccak digest, resets the transcript buffer to that digest, then samples +/// by interpreting the digest as a big-endian integer modulo r. +/// - Scalar inversion uses modexp(scalar, r-2, r). +/// - Constructors run deployment-time smoke tests for MCOPY and the EIP-2537 +/// precompiles using identity inputs. Compile with Solidity >=0.8.24 and +/// deploy only on chains/forks that support MCOPY and EIP-2537. +contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + + + /// @notice Verifying-key contract address authorized for this verifier. + /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. + address public immutable AUTHORIZED_VK; + // Expected VK runtime metadata. The deployed VK runtime is + // INVALID || payload, hence EXPECTED_VK_LENGTH is one byte longer than + // EXPECTED_VK_PAYLOAD_LENGTH. + uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 17024; + uint256 internal constant EXPECTED_VK_LENGTH = 17025; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x67bac137fa7e479c25b63324812752e4b6e13d9841d5bf83c322170bf91c0f88; + bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); + /// @notice Quotient evaluator contract authorized for split quotient reconstruction. + /// @dev The evaluator returns the linearization expected scalar and selector buckets; its runtime may be pinned by generated constants. + address public immutable AUTHORIZED_QUOTIENT; + // Expected split evaluator runtime metadata. It is checked at deployment + // and again immediately before each external quotient reconstruction. + uint256 internal constant EXPECTED_QUOTIENT_LENGTH = 9790; + uint256 internal constant EXPECTED_QUOTIENT_CODEHASH_WORD = 0x7e72c7c5d6fe845370d9431aaa590ab2cd62ca703c3d5b2a862bdb9937195814; + bytes32 internal constant EXPECTED_QUOTIENT_CODEHASH = bytes32(EXPECTED_QUOTIENT_CODEHASH_WORD); + + // Solidity ABI calldata cursors. The generated verifier accepts exactly + // verifyProof(bytes proof, uint256[] instances), then parses the `proof` + // bytes itself in the same order as the Rust verifier transcript. + uint256 internal constant PROOF_LEN_CPTR = 0x44; + uint256 internal constant PROOF_CPTR = 0x64; + uint256 internal constant NUM_INSTANCE_CPTR = 0x1ec4; + uint256 internal constant INSTANCE_CPTR = 0x1ee4; + // First general-purpose memory words reserved by the generated verifier. + // RETURN_MPTR is a single word set to 1 on success. + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; + + // ---------------------------------------------------------------------- + // Verifying-key memory map. The VK header lives at VK_MPTR, followed + // by the quotient VM payload and commitments. After the full VK + // runtime comes the challenge slots (challenge_mptr..) and the + // per-stage scratch (theta_mptr..). + // ---------------------------------------------------------------------- + uint256 internal constant VK_MPTR = 0x3680; + uint256 internal constant VK_DIGEST_MPTR = 0x3680; + uint256 internal constant NUM_INSTANCES_MPTR = 0x36a0; + uint256 internal constant K_MPTR = 0x36c0; + uint256 internal constant N_INV_MPTR = 0x36e0; + uint256 internal constant OMEGA_MPTR = 0x3700; + uint256 internal constant OMEGA_INV_MPTR = 0x3720; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x3740; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x3760; + uint256 internal constant ACC_OFFSET_MPTR = 0x3780; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x37a0; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x37c0; + uint256 internal constant G1_BASE_MPTR = 0x37e0; + uint256 internal constant G2_BASE_MPTR = 0x3860; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x3960; + + uint256 internal constant CHALLENGE_MPTR = 0x7900; + + // Challenge layout. Squeeze order in midnight-proofs: + // user_phase challenges (variable count) + // theta -> beta, gamma -> trash_challenge -> y -> x -> + // x1, x2 -> x3 -> x4 + uint256 internal constant THETA_MPTR = 0x7900; + uint256 internal constant BETA_MPTR = 0x7920; + uint256 internal constant GAMMA_MPTR = 0x7940; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x7960; + uint256 internal constant Y_MPTR = 0x7980; + uint256 internal constant X_MPTR = 0x79a0; + uint256 internal constant X1_MPTR = 0x79c0; + uint256 internal constant X2_MPTR = 0x79e0; + uint256 internal constant X3_MPTR = 0x7a00; + uint256 internal constant X4_MPTR = 0x7a20; + + // Batch-open commitments live in 4-word EIP-2537 padded slots. + uint256 internal constant F_COM_MPTR = 0x7a40; + uint256 internal constant PI_MPTR = 0x7ac0; + + // Accumulator (KZG IVC). + uint256 internal constant ACC_LHS_MPTR = 0x7b40; + uint256 internal constant ACC_RHS_MPTR = 0x7bc0; + + // Lagrange / linearization scratch. + uint256 internal constant X_N_MPTR = 0x7c40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x7c60; + uint256 internal constant L_LAST_MPTR = 0x7c80; + uint256 internal constant L_BLIND_MPTR = 0x7ca0; + uint256 internal constant L_0_MPTR = 0x7cc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x7ce0; + // Legacy name: this is not h(x). It stores the expected opening + // scalar for the linearized commitment, i.e. the negated y-batched + // identity numerator reconstructed from the alleged evals at x. + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x7d00; + uint256 internal constant QUOTIENT_MPTR = 0x7d20; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x7dc0; + uint256 internal constant V_MPTR = 0x7de0; + uint256 internal constant FINAL_COM_MPTR = 0x7e00; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x7e80; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x7f00; // 4 words + + // Multi-prepare scratch (sized at codegen time). + uint256 internal constant ROT_POINTS_MPTR = 0x7f80; + uint256 internal constant X1_POWERS_MPTR = 0x8300; + // Q_COM materialization is currently fused into the final MSM scratch, + // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero + // reserved capacity until a future emitter starts writing Q_COM_MPTR. + uint256 internal constant Q_COM_MPTR = 0x8b20; + uint256 internal constant Q_EVAL_SET_MPTR = 0x8b20; + + // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals + // block of the proof; we keep it as a memory slot for symmetry. + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x9220; + + // Reserved 4-word slot for the G1 identity (point at infinity) in + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x9320; + + // Decoded polynomial-eval buffer (Optimisation H3). The off-chain + // Solidity proof shim rewrites proof scalars into canonical BE words, + // so `calldataload` gives the field element directly. The transcript- + // side `evaluations` loop range-checks and spills that value here so + // downstream eval references (gate evaluator + PCS q_eval Horner) + // become 3-gas `mload(...)` instead of calldata reads. + uint256 internal constant REVERSED_EVALS_MPTR = 0x9480; + uint256 internal constant SELECTOR_ACC_MPTR = 0xb140; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0xb140; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0xb4e0; + uint256 internal constant TRACE_U256_MPTR = 0xe340; + + // ---------------------------------------------------------------------- + // Per-category bases for EIP-2537 padded G1 commitments. The proof + // calldata carries 128-byte uncompressed/padded G1s after the off-chain + // proof shim repacks midnight-proofs' native compressed stream; this + // region stores the 4-word slots used by PCS / quotient-fold sections. + // + // Cumulative offsets (in words from `comms_mptr_base`): + // ADVICE_COMMS_MPTR_BASE + 0 + // LOOKUP_M_COMMS_MPTR_BASE + 4*total_advices + // PERM_Z_COMMS_MPTR_BASE + 4*total_advices + 4*num_lookups + // LOOKUP_HELPER_COMMS_MPTR_BASE + ... + 4*num_permutation_zs + // LOOKUP_Z_COMMS_MPTR_BASE + ... + 4*lookup_helper_chunks_total + // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups + // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans + // ---------------------------------------------------------------------- + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0xa140; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0xa8c0; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0xa9c0; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0xacc0; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0xadc0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0xaec0; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0xaf40; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 525096; + // Worst-case accumulator RHS MSM: carried RHS point plus every generated + // fixed-base tail scalar nonzero. Zero tail scalars are omitted at + // runtime, which only lowers the actual cost below this bound. + uint256 internal constant ACC_RHS_MSM_GAS = 12000; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x5c2e8be9a8dc4e220b823ce41569f6a757baefeed4df0a04ef5e67db74b36d27; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; + + // BLS12-381 scalar-field modulus, used for transcript challenges and all + // Halo2 verifier arithmetic. + uint256 internal constant FR_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // BLS12-381 Fp modulus minus one, split like an EIP-2537 coordinate: + // high word = 16 zero bytes || top 16 coordinate bytes, low word = + // bottom 32 coordinate bytes. + uint256 internal constant BLS_P_HI = 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7; + uint256 internal constant BLS_P_MINUS_ONE_LO = 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + // Packed public-accumulator sentinels for the shifted coordinate codec. + // The `_WITH_ID_FLAG` variant is used only for the first x-coordinate word. + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0 = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_1 = 0x0000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd764774b84; + + /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. + /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + + // Scratch is reused for every runtime-prerequisite probe. + let scratch := 0x1000 + + // MCOPY must be available because the verifier uses it for + // proof-time point/scratch staging. Execute the opcode here so a + // non-Cancun fork fails during deployment instead of later proofs. + mstore(scratch, 0x1234) + mcopy(add(scratch, 0x20), scratch, 0x20) + if iszero(eq(mload(add(scratch, 0x20)), 0x1234)) { revert(0, 0) } + + // Start the EIP-2537 probes with the identity encoding for G1/G2: + // all-zero padded words. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + + // G1ADD(identity, identity) -> identity, 128-byte return. + // This catches chains where the precompile is missing or returns a + // non-standard success shape. + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + + // Worst-case generated G1MSM with all identity/zero terms -> + // identity, 128-byte return. This exercises the largest MSM input + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0xb140 + for { let off := 0 } lt(off, 0x30c0) { off := add(off, 0x20) } { + mstore(add(msm_scratch, off), 0) + } + // The production verifier uses G1MSM both for commitments and as + // the subgroup validator for absorbed proof points. + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x30c0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)]) + // -> true, 32-byte return. This matches the runtime two-pair KZG + // pairing input size and catches absent pairing precompiles, + // short return data, and obviously incompatible semantics. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(scratch), 1)) { revert(0, 0) } + } + } + + + /// @notice Create a verifier pinned to a verifying key and quotient evaluator. + /// @dev Checks MCOPY/EIP-2537 availability and verifies both dependency runtimes before storing their addresses. + /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. + /// @param authorizedQuotient Address of the generated `Halo2QuotientEvaluator` runtime. + constructor(address authorizedVk, address authorizedQuotient) { + // Verifier correctness depends on chain support for MCOPY and the + // BLS12-381 precompiles; fail deployment before pinning dependencies. + require_eip2537_precompiles(); + // Pin the generated VK runtime exactly. The verifier later repeats the + // codehash/length check before copying the VK payload for a proof. + require( + authorizedVk.code.length == EXPECTED_VK_LENGTH + && authorizedVk.codehash == EXPECTED_VK_CODEHASH, + "invalid vk" + ); + // The split evaluator contains generated verifier logic, not a generic + // library. Pin it with the same strictness as the verifying key. + require( + authorizedQuotient.code.length == EXPECTED_QUOTIENT_LENGTH + && authorizedQuotient.codehash == EXPECTED_QUOTIENT_CODEHASH, + "invalid quotient" + ); + // Store the already-validated dependency addresses for proof-time + // memory loading and quotient reconstruction. + AUTHORIZED_VK = authorizedVk; + AUTHORIZED_QUOTIENT = authorizedQuotient; + } + + /// @notice Verify a Halo2/Midfall proof for the generated verifying key. + /// @dev This checks only that `proof` verifies for the supplied public + /// `instances` under this pinned VK/protocol. Application contracts must + /// bind the meaning of those instances separately: state roots, program + /// identifiers, expected IVC outputs, chain/domain separation, and any + /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. + /// @dev Production renders are success-or-revert: accepted proofs return + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. + /// @dev The generated verifier uses absolute Yul memory addresses instead + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. + function verifyProof( + bytes calldata proof, + uint256[] calldata instances + ) external returns (bool) { + // Cheap ABI-shape guard before any generated memory work: + // - proof head must point at the bytes payload; + // - instances head must point at the generated instance array. + // + // The verifier below is a hand-rolled calldata parser. Failing here + // keeps malformed dynamic-argument layouts from being interpreted as a + // valid Midfall proof stream. + assembly ("memory-safe") { + if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) + } + } + // Non-embedded renders pin the VK by address and codehash. The Yul + // loader rechecks the runtime before every proof and copies the + // INVALID-prefixed payload into VK_MPTR. + address vk = AUTHORIZED_VK; + // Split quotient renders delegate the scalar-side identity numerator + // reconstruction to a separately deployed generated evaluator. + address quotientEvaluator = AUTHORIZED_QUOTIENT; + assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + + // This block owns the call-frame memory and remains terminal. + // Generated scratch starts at TRANSCRIPT_MPTR, preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot + // words. See docs/architecture/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== + + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // calls this only after transcript absorption is complete, so it + // reuses the dead transcript buffer just below VK_MPTR instead of + // a fixed post-VK address that can collide with live PCS scratch + // when the VK payload becomes smaller. + function scalar_inv(x) -> inv { + // Zero has no multiplicative inverse in Fr; callers rely on a + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x3580 + // EIP-198 modexp frame: + // [base_len, exp_len, mod_len, base, exponent, modulus] + mstore(add(p, 0x00), 0x20) // base len + mstore(add(p, 0x20), 0x20) // exp len + mstore(add(p, 0x40), 0x20) // mod len + mstore(add(p, 0x60), x) + mstore(add(p, 0x80), sub(FR_MODULUS, 2)) + mstore(add(p, 0xa0), FR_MODULUS) + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + inv := mload(p) + } + + // ---------- Streaming Keccak256 transcript helpers ---------- + // + // The transcript buffer lives at + // memory[TRANSCRIPT_MPTR..buf_len). On verifier entry it starts + // empty. Each common(input) appends raw bytes. squeeze_*(buf_len) + // computes one Keccak digest, reseeds the buffer with that + // 32-byte digest, and samples a Fq element as + // uint256(digest_be) mod r. + + function transcript_init() -> buf_len { + // Empty transcript buffer starts exactly at TRANSCRIPT_MPTR. + buf_len := TRANSCRIPT_MPTR + } + + // Append one 32-byte big-endian field/transcript word at the + // current end of the transcript buffer. + function common_word(buf_len, word) -> ret { + mstore(buf_len, word) + ret := add(buf_len, 32) + } + + // Absorb a BLS12-381 G1 point in EIP-2537 padded + // uncompressed form (4 calldata words = 128 bytes: + // x_hi || x_lo || y_hi || y_lo, each coord = 16 zero + // pad bytes + 48 big-endian field bytes) into the + // transcript buffer at `buf_len`. + // + // Matches the patched `Hashable for + // midnight_curves::G1Projective::to_input` in + // midnight-proofs, which now emits the same 128-byte form + // (`midfall/proofs/src/transcript/implementors.rs`). The + // previous emitter hashed the 48-byte ZCash compressed + // encoding instead and ran a 384-bit `lex(y) > lex(p − y)` + // ladder + identity flag fixup to derive the sign bit on + // the fly; switching to the uncompressed form drops that + // ladder entirely. + // + // Canonicality: reject non-zero bytes in the top 16 bytes + // of each `_hi` calldata word and reject coordinates + // outside Fp. Normalizing those bytes before hashing would + // make multiple calldata encodings share one transcript. + // + // This helper does not run an independent curve/subgroup + // check. Instead, ProtocolPlan::validate rejects generated + // plans where an absorbed proof commitment would not later be + // consumed by an EIP-2537 G1MSM or pairing path, and those + // precompiles perform the curve/subgroup validation. + // + // The point's uncompressed form remains in calldata; the + // call site is responsible for `calldatacopy`-ing it into + // memory afterwards if it needs the on-curve coordinates. + function common_uncompressed_g1(buf_len, cptr) -> ret { + let x_hi_word := calldataload(cptr) + let x_lo := calldataload(add(cptr, 0x20)) + let y_hi_word := calldataload(add(cptr, 0x40)) + let y_lo := calldataload(add(cptr, 0x60)) + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + + let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) + let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) + if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { + fail(ERR_BAD_POINT_ENCODING) + } + if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { + fail(ERR_BAD_POINT_ENCODING) + } + + // Memcpy the 4 calldata words (128 bytes) verbatim + // into the keccak buffer. + calldatacopy(buf_len, cptr, 0x80) + ret := add(buf_len, 0x80) + } + + // One Keccak finalization + reseed. Returns the new buffer + // cursor (= TRANSCRIPT_MPTR + 32) and stores the squeezed Fq at + // `mptr`. + function squeeze_to(buf_len, mptr) -> ret { + let h0 := keccak256(TRANSCRIPT_MPTR, sub(buf_len, TRANSCRIPT_MPTR)) + // Reseed: write the 32-byte digest at start of buffer. + mstore(TRANSCRIPT_MPTR, h0) + let r := FR_MODULUS + // Sample Fq as uint256(keccak_digest_be) mod r. + mstore(mptr, mod(h0, r)) + ret := add(TRANSCRIPT_MPTR, 32) + } + + // ---------- EC primitives (EIP-2537 wrappers) ---------- + // + // These mirror the BN254 helpers but operate on 4-word G1 + // points. They use planned memory windows above Solidity's + // reserved prefix; the streaming transcript buffer is no longer + // needed once all challenges are squeezed. + + // Invert a contiguous run of Fr words in-place using Montgomery's + // batch inversion trick: + // 1. write prefix products to scratch; + // 2. invert the total product once with modexp; + // 3. walk backward to recover each individual inverse. + // + // The function returns a boolean instead of reverting so callers + // can combine it with other `success` plumbing until a section + // boundary decides whether to fail closed. + function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { + ret := success + if iszero(ret) { leave } + // Memory ranges must be forward and word-aligned by + // construction; a reversed range is always a codegen error. + if lt(mptr_end, mptr_start) { + ret := 0 + leave + } + + let count_bytes := sub(mptr_end, mptr_start) + // Empty batch is valid and leaves memory untouched. + if iszero(count_bytes) { leave } + + // Fast path for a single denominator: avoid prefix scratch and + // just run one modexp inverse in place. + if eq(count_bytes, 0x20) { + let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } + if iszero(x) { + ret := 0 + leave + } + + let single_scratch := scratch_mptr + mstore(add(single_scratch, 0x00), 0x20) + mstore(add(single_scratch, 0x20), 0x20) + mstore(add(single_scratch, 0x40), 0x20) + mstore(add(single_scratch, 0x60), x) + mstore(add(single_scratch, 0x80), sub(r, 2)) + mstore(add(single_scratch, 0xa0), r) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + if ret { mstore(mptr_start, mload(single_scratch)) } + leave + } + + // Forward pass: scratch stores prefix products up to, but not + // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. + let gp_mptr := scratch_mptr + let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } + let mptr := add(mptr_start, 0x20) + for {} lt(mptr, sub(mptr_end, 0x20)) {} { + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) + mstore(gp_mptr, gp) + mptr := add(mptr, 0x20) + gp_mptr := add(gp_mptr, 0x20) + } + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) + // A zero total product means at least one denominator was + // zero, so no batch inverse exists. + if iszero(gp) { + ret := 0 + leave + } + + // Invert the total product once. + mstore(add(gp_mptr, 0x00), 0x20) + mstore(add(gp_mptr, 0x20), 0x20) + mstore(add(gp_mptr, 0x40), 0x20) + mstore(add(gp_mptr, 0x60), gp) + mstore(add(gp_mptr, 0x80), sub(r, 2)) + mstore(add(gp_mptr, 0xa0), r) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } + let all_inv := mload(gp_mptr) + + // Backward pass: derive each inverse from the inverted total + // product and the saved prefix products. + let first_mptr := mptr_start + let second_mptr := add(first_mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + for {} lt(second_mptr, mptr) {} { + let inv := mulmod(all_inv, mload(gp_mptr), r) + all_inv := mulmod(all_inv, mload(mptr), r) + mstore(mptr, inv) + mptr := sub(mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + } + let inv_first := mulmod(all_inv, mload(second_mptr), r) + let inv_second := mulmod(all_inv, mload(first_mptr), r) + mstore(first_mptr, inv_first) + mstore(second_mptr, inv_second) + } + + // Final EIP-2537 pairing wrapper. `lhs_mptr` and `rhs_mptr` are + // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. + function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { + ret := success + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } + // Lay out two (G1, G2) pairs at scratch..scratch+0x300: + // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] + // Cancun MCOPY (3 + 3·words gas) replaces what used to + // be a 4-step mstore chain for each G1 (~60 gas) and an + // 8-iter mstore loop for each G2 (~240 gas). Net saving + // here is ~500 gas per ec_pairing call. + let scratch := 0x1240 + mcopy(scratch, lhs_mptr, 0x80) + mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) + mcopy(add(scratch, 0x180), rhs_mptr, 0x80) + mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } + ret := 1 + } + + // ---------- IVC accumulator public-input decoding ---------- + // + // `AssignedForeignPoint` exposes each base-field coordinate + // through `AssignedField::as_public_input`: seven radix-2^56 limbs of + // (coord - 1) are packed four-at-a-time into native field elements. + // The x coordinate's first packed word carries the identity flag by + // adding one raw radix base. Rebuild EIP-2537 padded + // (x_hi, x_lo, y_hi, y_lo) words from that encoding. + // + // Public-input layout for one coordinate: + // word 0: limb_0 | limb_1 << bits | ... up to limbs_per_word + // word 1: next limbs, if any + // + // The limbs are little-endian in the represented integer even + // though calldata words are loaded as big 256-bit values. The loop + // below extracts each limb by shifting inside the packed word and + // reconstructs the full coordinate into the two-word EIP-2537 + // representation expected by the BLS12-381 precompiles. + function load_acc_coord_shifted(src, bits, n, base, limbs_per_word, first_adjust) -> hi, lo { + // Mask for one radix limb, e.g. 2^56 - 1 for the current + // BLS12-381 self-emulation parameters. + let mask := sub(base, 1) + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + // Limb words are little-endian packed inside each Fr + // public input. `first_adjust` removes the identity flag + // base from the first x word when present. + let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { + packed := sub(packed, first_adjust) + } + // Select limb i from its packed field word. The mod/div + // pair maps a limb index to an intra-word limb slot and + // the calldata word containing it. + let limb := and(shr(mul(mod(i, limbs_per_word), bits), packed), mask) + + let shift := mul(i, bits) + // Split the reconstructed 384-bit coordinate into the + // EIP-2537 high/low words expected by the precompiles. + if lt(shift, 256) { + lo := add(lo, shl(shift, limb)) + if gt(add(shift, bits), 256) { + // A limb can straddle the 256-bit low/high split. + // Move the overflow bits into hi. + hi := add(hi, shr(sub(256, shift), limb)) + } + } + if iszero(lt(shift, 256)) { + // Once shift >= 256 the whole limb belongs to hi. + hi := add(hi, shl(sub(shift, 256), limb)) + } + } + } + + // The shifted coordinate codec represents zero as p-1 before the + // final +1 below, so keep this sentinel explicit. + function is_bls_p_minus_one(hi, lo) -> yes { + yes := and(eq(hi, BLS_P_HI), eq(lo, BLS_P_MINUS_ONE_LO)) + } + + // Canonical encoded accumulator identity: + // x = p-1 plus the identity flag in the first packed word, + // y = p-1 with no identity flag. + // It decodes to the EIP-2537 point-at-infinity slot (all zeros). + // + // This fast path is deliberately stricter than "decodes to zero": + // the point at infinity has exactly one accepted public-input + // encoding. Non-canonical zero-like encodings are rejected later. + function is_acc_encoded_identity(src) -> yes { + yes := and( + and( + eq(calldataload(src), BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG), + eq(calldataload(add(src, 0x20)), BLS_P_MINUS_ONE_PACKED_1) + ), + and( + eq(calldataload(add(src, 0x40)), BLS_P_MINUS_ONE_PACKED_0), + eq(calldataload(add(src, 0x60)), BLS_P_MINUS_ONE_PACKED_1) + ) + ) + } + + // Reject unused high bits in the packed public-input words. This + // makes each accumulator point encoding canonical before it reaches + // the precompile-based curve/subgroup validation. + function check_acc_coord_packing(src, bits, n, limbs_per_word) -> ok { + ok := 1 + // Number of packed native-field public-input words occupied by + // one coordinate. + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + for { let word_idx := 0 } lt(word_idx, coord_words) { word_idx := add(word_idx, 1) } { + // The final word may contain fewer than limbs_per_word + // limbs. Any unused high bits must be zero, otherwise the + // same coordinate would have multiple calldata encodings. + let remaining := sub(n, mul(word_idx, limbs_per_word)) + let limbs_in_word := limbs_per_word + if lt(remaining, limbs_per_word) { + limbs_in_word := remaining + } + let used_bits := mul(limbs_in_word, bits) + if lt(used_bits, 256) { + // shl(used_bits, 1) == 2^used_bits. The packed word + // must be strictly less than that bound. + ok := and(ok, lt(calldataload(add(src, mul(word_idx, 0x20))), shl(used_bits, 1))) + } + } + } + + // Decode one shifted coordinate. `allow_id` is true only for x, + // because the identity flag lives in x's first packed word. + function load_acc_coord(src, allow_id, bits, n, base, limbs_per_word) -> ok, hi, lo, is_id { + ok := check_acc_coord_packing(src, bits, n, limbs_per_word) + if and(allow_id, iszero(lt(calldataload(src), base))) { + // Probe the x identity flag by removing one radix base and + // checking whether the adjusted coordinate is p-1. + // + // `calldataload(src) >= base` is a cheap prefilter: only x + // can carry this flag, and adding one radix base must make + // the first packed word at least base. + let adj_hi, adj_lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, base) + is_id := is_bls_p_minus_one(adj_hi, adj_lo) + } + + // Decode again with the identity adjustment applied only when + // the canonical identity flag was actually detected. + hi, lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, mul(is_id, base)) + ok := and( + ok, + // Coordinate must be in the BLS12-381 base field, i.e. + // <= p - 1 in split hi/lo form. + or(lt(hi, BLS_P_HI), and(eq(hi, BLS_P_HI), iszero(gt(lo, BLS_P_MINUS_ONE_LO)))) + ) + + let was_p_minus_one := is_bls_p_minus_one(hi, lo) + if was_p_minus_one { + // Shifted encoding maps p-1 back to zero. + hi := 0 + lo := 0 + } + if iszero(was_p_minus_one) { + // All other coordinates are encoded as coord - 1, so add + // one back with carry into the high word. + let next_lo := add(lo, 1) + hi := add(hi, lt(next_lo, lo)) + lo := next_lo + } + + // EIP-2537 pads each 48-byte Fp coordinate to 64 bytes, + // so the high word must fit in its low 128 bits. + // This also catches impossible reconstructions above 384 bits. + ok := and(ok, lt(hi, shl(128, 1))) + } + + // Decode a public accumulator point into an EIP-2537 4-word G1 + // slot. Non-identity points are curve/subgroup checked later by + // routing them through G1MSM. + function load_acc_point(dst, src, bits, n, base) -> ok, is_id { + // Prefer the canonical all-coordinate identity encoding before + // attempting coordinate-level shifted decoding. This accepts + // the point at infinity only in the exact form generated by the + // circuit's public-input codec. + is_id := is_acc_encoded_identity(src) + if is_id { + ok := 1 + // EIP-2537 encodes G1 identity as four zero words: + // x_hi = x_lo = y_hi = y_lo = 0. + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // x occupies coord_words packed public-input words; y + // starts immediately after x. + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // Only x may carry the identity flag. y must decode as a + // normal shifted coordinate. + let x_ok, x_hi, x_lo, x_is_id := load_acc_coord(src, 1, bits, n, base, limbs_per_word) + let y_ok, y_hi, y_lo, y_id := load_acc_coord( + add(src, mul(coord_words, 0x20)), + 0, + bits, + n, + base, + limbs_per_word + ) + // y_id is always zero because allow_id was false, but the + // tuple shape is shared with x decoding. + pop(y_id) + ok := and(x_ok, y_ok) + is_id := x_is_id + + if is_id { + // If x carried the identity flag, both decoded + // coordinates must be zero after shifting. Any other y + // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. + ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // The coordinate codec maps encoded p-1 to decoded + // zero. EIP-2537 reserves affine (0,0) for the point + // at infinity, so a decoded infinity is only valid + // when the canonical accumulator identity encoding + // was used above. + let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo))) + ok := and(ok, iszero(decoded_zero)) + // Store the affine point in the exact precompile input + // layout: x_hi, x_lo, y_hi, y_lo. + mstore(dst, x_hi) + mstore(add(dst, 0x20), x_lo) + mstore(add(dst, 0x40), y_hi) + mstore(add(dst, 0x60), y_lo) + } + } + } + // Validate and prepare the public accumulator equation before the + // main transcript starts. This fails malformed public inputs early + // and writes ACC_LHS_MPTR / ACC_RHS_MPTR for final pairing batching. + // + // The accumulator public input represents an equality of two G1 + // commitments used by the recursive KZG accumulator. This helper: + // 1. decodes carried public G1 points from shifted limbs; + // 2. forces every decoded point through EIP-2537 G1MSM so the + // precompile validates curve/subgroup membership; + // 3. folds the RHS carried point and fixed-base scalar tail into + // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for + // randomized batching in FinalPairing.yul. + // `r` is consumed only by the canonicality guards in the + // carried-scalar and fixed-base-tail arms; renders whose + // accumulator layout has neither (e.g. point_pair with no tail) + // legally leave it unused. + function validate_public_accumulator(success, r) -> out { + out := success + let bits := 56 + let n := 7 + // The BLS12-381 self-emulation currently exposes Fp + // coordinates as 7 radix-2^56 limbs. + let limb_base := shl(bits, 1) + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // acc_offset is generated from the VK/protocol shape and + // points into the ABI `instances` array. + let acc_instance_ptr := add(INSTANCE_CPTR, 0x80) + + // LHS layout: point limbs (x,y), then either an explicit + // scalar word or an implicit unit scalar for already-collapsed + // point-pair public inputs. + // The scalar pointer is computed unconditionally; the rendered + // branch below decides whether to read it or use scalar 1. + let lhs_scalar_ptr := add(acc_instance_ptr, mul(mul(2, coord_words), 0x20)) + let lhs_ok, lhs_is_id := load_acc_point(ACC_LHS_MPTR, acc_instance_ptr, bits, n, limb_base) + out := and(out, lhs_ok) + // Shared scratch for one-pair LHS validation and the later + // variable-length RHS MSM. + let acc_scratch := 0xb140 + { + // Carried-scalar layout: the circuit exposes the scalar + // that multiplies the carried LHS point. + let lhs_scalar := calldataload(lhs_scalar_ptr) + // Canonicality is enforced here rather than relying on the + // later instance-absorption loop: G1MSM reduces scalars + // mod r implicitly, so s and s+r would be indistinguishable + // inside this helper. + out := and(out, lt(lhs_scalar, r)) + // Identity status is useful for decoding checks above, but + // validation still goes through G1MSM for all points. + pop(lhs_is_id) + // Always route the decoded carried point through G1MSM, + // even for identity points and zero/one scalars. The + // precompile is the on-curve/subgroup validator for this + // public-input point; skipping it would let a malformed + // non-identity point hide behind scalar 0. + mcopy(acc_scratch, ACC_LHS_MPTR, 0x80) + mstore(add(acc_scratch, 0x80), lhs_scalar) + if out { + // Single-pair MSM output overwrites ACC_LHS_MPTR with + // lhs_scalar * decoded_lhs. If lhs_scalar is one, this + // is also a curve/subgroup validation round-trip. + out := staticcall(G1MSM_GAS_1PAIR, 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // RHS layout for this generated verifier is fully collapsed: + // point limbs (x,y), scalar. There is no fixed-base scalar + // tail; fixed-base contributions were already folded into + // ACC_RHS by the circuit/native accumulator construction. + let rhs_instance_ptr := add(lhs_scalar_ptr, 0x20) + // RHS scalar, when present, immediately follows the RHS point + // limbs. The fixed-base scalar tail starts after it. + let rhs_scalar_ptr := add(rhs_instance_ptr, mul(mul(2, coord_words), 0x20)) + let rhs_ok, rhs_is_id := load_acc_point(ACC_RHS_MPTR, rhs_instance_ptr, bits, n, limb_base) + out := and(out, rhs_ok) + // acc_pair_ptr appends (G1, scalar) pairs into acc_scratch for + // one final RHS MSM. + let acc_pair_ptr := acc_scratch + { + // Explicit carried RHS scalar. + let rhs_scalar := calldataload(rhs_scalar_ptr) + out := and(out, lt(rhs_scalar, r)) + pop(rhs_is_id) + // Keep the carried RHS point in the MSM input even when + // it is encoded as identity or has scalar 0/1, so EIP-2537 + // validates every decoded public accumulator point before + // it can affect, or be erased from, the pairing batch. + mcopy(acc_pair_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(acc_pair_ptr, 0x80), rhs_scalar) + // Move to the next (G1, scalar) pair slot. + acc_pair_ptr := add(acc_pair_ptr, 0xa0) + } + // Total byte length of the appended RHS MSM input pairs. This + // is at least one pair because the carried RHS point is always + // appended; keep the guard for synthetic render configurations. + let acc_msm_len := sub(acc_pair_ptr, acc_scratch) + if acc_msm_len { + // Fold the carried RHS point and any generated fixed-base + // tail into ACC_RHS_MPTR. The later final pairing block + // randomizes this equation together with the KZG pairing. + if out { + // Output overwrites ACC_RHS_MPTR with: + // rhs_scalar * carried_rhs + // + sum_i fixed_scalar_i * fixed_base_i + // + // The precompile also validates every nonzero fixed + // base embedded by codegen and the carried RHS point. + // ACC_RHS_MSM_GAS is the compile-time worst case + // (every tail scalar nonzero); acc_msm_len can only + // select a same-size-or-smaller MSM at runtime. + out := staticcall( + ACC_RHS_MSM_GAS, + 0x0c, + acc_scratch, + acc_msm_len, + ACC_RHS_MPTR, + 0x80 + ) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // The caller checks `out` and reverts before transcript work if + // any decode, canonicality, or precompile validation failed. + } + + + // Section-boundary gas-attribution checkpoint. Emits a + // single LOG1 (no data) with topic = (id << 248) | gas(). + // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. + // Host-side parses the topic into (id, gas_left) and prints + // pairwise deltas (see `dump_gas_checkpoints`). + function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) + } + + let r := FR_MODULUS + let success := true + + + gas_checkpoint(1) // entry: before VK loading + + // =============================================================== + // VK loading: either bake in the embedded VK bytes or fetch + // them from the linked AUTHORIZED_VK contract. + // + // This is the first verifier phase after helper definitions. Its + // job is to make the generated VK payload available at VK_MPTR in + // one canonical memory layout, regardless of whether this render + // embeds the VK directly or links a separate Halo2VerifyingKey + // contract. + // + // Later template partials treat VK_MPTR as already populated with: + // - header words: vk_digest, domain data, accumulator metadata; + // - BLS12-381 base points used by the final pairing; + // - compact quotient VM constants/program bytes, when enabled; + // - fixed and permutation commitments in 4-word G1 slots. + // =============================================================== + { + // Re-check the pinned VK dependency on every proof. The + // constructor check catches normal deployment mistakes, while + // this fresh check hardens forks or same-transaction edge + // cases where code at the authorized address could differ + // from the runtime originally pinned by this verifier. + // + // EXPECTED_VK_LENGTH includes the leading INVALID byte in the + // Halo2VerifyingKey runtime. EXPECTED_VK_CODEHASH_WORD is the + // full runtime hash, not only the payload hash. + if iszero(and( + eq(extcodesize(vk), EXPECTED_VK_LENGTH), + eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) + )) { fail(ERR_VK_MISMATCH) } + // Runtime byte 0 is INVALID so direct calls cannot execute the + // payload. Copy from byte 1 into VK_MPTR to reconstruct the + // exact payload layout used by the embedded branch. + extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) + + // Cross-check loaded VK header words against the verifier + // constants used by later parser, domain, and accumulator + // paths. Codehash pinning protects the external VK address; + // these checks catch generator drift before calldata parsing + // chooses a stale schema. + success := and(success, eq(mload(NUM_INSTANCES_MPTR), 14)) + success := and(success, eq(mload(K_MPTR), 20)) + success := and(success, eq(mload(HAS_ACCUMULATOR_MPTR), 1)) + success := and(success, eq(mload(ACC_OFFSET_MPTR), 4)) + success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 7)) + success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) + if iszero(success) { fail(ERR_VK_MISMATCH) } + // + // The checks below validate the dynamic ABI envelope before the + // transcript parser starts walking raw calldata: + // - proof bytes length equals the generated proof layout; + // - instance array length equals the generated public input + // count; + // - total calldata length has no missing or trailing words. + // + // `success` is folded through `and` for consistency with later + // sections, then immediately enforced at the end of this block. + // A failure here means the verifier is not looking at the proof + // shape it was generated to parse. + success := and(success, eq(0x1e60, calldataload(PROOF_LEN_CPTR))) + success := and(success, eq(14, calldataload(NUM_INSTANCE_CPTR))) + // Calldata must contain exactly the ABI selector, proof bytes, + // instance-array length, and generated number of instance + // words. Any trailing bytes fail closed. + success := and( + success, + eq(calldatasize(), add(INSTANCE_CPTR, 0x01c0)) + ) + // Stop before any transcript absorption if the ABI/proof shape + // is not exactly the generated one. + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } + } + // Fail malformed accumulator public inputs before transcript, + // quotient, PCS, and final pairing work. The late accumulator block + // only batches these already-validated G1 outputs into the final + // pairing equation. + // + // Accumulator validation decodes shifted public-input limbs into + // EIP-2537 G1 slots, checks canonical encodings, and routes points + // through G1MSM for curve/subgroup validation. Doing it here means + // invalid accumulator public inputs cannot influence transcript + // challenge derivation or waste gas in later quotient/PCS work. + // validate_public_accumulator returns a boolean to share the same + // success-plumbing style as other helper calls; this boundary is + // where the verifier converts failure to a revert. + success := validate_public_accumulator(success, r) + if iszero(success) { fail(ERR_BAD_POINT_ENCODING) } + gas_checkpoint(2) // after VK loading + accumulator public-input precheck + + // =============================================================== + // Transcript: VK digest + instances + proof. + // + // This block is the Solidity mirror of the native Midfall verifier + // transcript schedule. It does three jobs at once: + // + // 1. Absorb public data and proof bytes into the streaming + // Keccak transcript in exactly the native order. + // 2. Decode/range-check proof scalars and canonical G1 calldata. + // 3. Copy proof commitments/evaluations into planned memory + // slots consumed by Lagrange, quotient, PCS, and pairing + // blocks later in the verifier. + // + // `buf_len` is a write cursor into the transcript buffer. The + // helper functions append bytes and return the new cursor; squeeze + // helpers hash memory[TRANSCRIPT_MPTR..buf_len), reseed the buffer + // with the digest, and write the sampled Fr challenge to memory. + // =============================================================== + let buf_len := transcript_init() + // VK_DIGEST_MPTR holds the digest as a BE 32-byte word (the + // VK contract stores it via `mstore`, which matches the + // Keccak Fq transcript input). + // + // This digest commits to the verifier key / constraint system + // before any proof material is read. + buf_len := common_word(buf_len, mload(VK_DIGEST_MPTR)) + + // Absorb committed_pi = G1Affine::identity() when the + // `committed-instances` feature is on in midnight-proofs. + // Under the patched `Hashable::to_input` (see + // `midfall/proofs/src/transcript/implementors.rs`), the + // identity hashes as 128 zero bytes (EIP-2537 (0,0) + // convention), NOT the 48-byte ZCash compressed form + // 0xc0||47*0x00 that the previous emitter produced. + // Native verifier absorbs this BEFORE the instance count. + { + // 128 zero bytes: zero out 4 consecutive 32-byte words + // at buf_len. + // This is a raw transcript absorb, not a memory slot kept for + // later elliptic-curve operations. + mstore(buf_len, 0) + mstore(add(buf_len, 0x20), 0) + mstore(add(buf_len, 0x40), 0) + mstore(add(buf_len, 0x60), 0) + buf_len := add(buf_len, 0x80) + } + + { + // Native verifier absorbs a length scalar before instance + // values; Keccak Fq transcript input is canonical BE. + // The ABI length was already checked against this generated + // constant in VkLoading.yul. + buf_len := common_word(buf_len, 14) + + let instance_cptr := INSTANCE_CPTR + for { let instance_cptr_end := add(instance_cptr, 0x01c0) } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) } { + let inst_be := calldataload(instance_cptr) + // Public inputs are BLS12-381 scalar-field elements. They + // must be canonical before transcript absorption; accepting + // non-canonical encodings would admit transcript aliases. + success := and(success, lt(inst_be, r)) + // Instances are passed BE in calldata, matching the + // Keccak Fq transcript input. + buf_len := common_word(buf_len, inst_be) + } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } + } + gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs + + // =============================================================== + // Per-user-phase reads + challenge squeezes. + // + // Each proof G1 is already EIP-2537 padded in calldata. The + // verifier validates and absorbs that 128-byte form, then copies + // it into the corresponding per-category MPTR. The PCS / + // quotient-fold blocks below dereference those MPTRs. + // + // All G1 reads follow the same pattern: + // - common_uncompressed_g1 canonicalizes/range-checks the two Fp + // coordinates and appends the exact 128 calldata bytes; + // - calldatacopy stores the same 4-word G1 slot in planned + // memory for later EIP-2537 precompile calls; + // - proof_cptr advances by one G1 byte length. + // =============================================================== + // proof_cptr walks the raw proof bytes inside the ABI `bytes` + // payload. Every successful read advances it exactly once, and the + // final equality check below proves the parser consumed the whole + // generated proof layout. + let proof_cptr := PROOF_CPTR + // advice_walk mirrors proof commitment order into the contiguous + // G1 commitment memory region used by PCS and quotient folding. + let advice_walk := ADVICE_COMMS_MPTR_BASE + // ---- User phase 1 ---- + // Advice commitments for this phase are absorbed before the phase's + // challenge squeezes. The number of commitments and challenges is + // generated from the protocol plan. + for { let end := add(proof_cptr, 0x0780) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + // Store the commitment at its phase-ordered advice slot. + calldatacopy(advice_walk, proof_cptr, 0x80) + advice_walk := add(advice_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes + + // ---- theta ---- + // From this point onward the transcript alternates between + // squeezed challenges and proof commitments exactly as + // midnight-proofs does in `plonk/verifier.rs`. + // theta batches lookup input expressions. + buf_len := squeeze_to(buf_len, THETA_MPTR) + // ---- multiplicities (one G1 per lookup) ---- + // Lookup multiplicity commitments are absorbed after theta and + // copied into their own contiguous G1 region. + let lookup_m_walk := LOOKUP_M_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0100) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_m_walk, proof_cptr, 0x80) + lookup_m_walk := add(lookup_m_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(5) // after theta squeeze + lookup multiplicities + + // ---- beta, gamma ---- + // beta and gamma are the permutation/lookup randomizers. They are + // squeezed after lookup multiplicities and before permutation + // product commitments, matching the native verifier schedule. + buf_len := squeeze_to(buf_len, BETA_MPTR) + buf_len := squeeze_to(buf_len, GAMMA_MPTR) + // ---- permutation Z products ---- + // Permutation product commitments are used by the permutation + // identities in the quotient numerator and later by PCS openings. + let perm_z_walk := PERM_Z_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0300) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(perm_z_walk, proof_cptr, 0x80) + perm_z_walk := add(perm_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(6) // after beta/gamma + permutation Z products + // ---- lookup helpers + accumulators (per-lookup) ---- + // Each lookup contributes zero or more helper commitments followed + // by its lookup accumulator Z commitment. The generated layout keeps + // helper commitments and accumulator commitments in separate memory + // regions because the quotient/PCS schedules address them + // differently. + let lookup_helper_walk := LOOKUP_HELPER_COMMS_MPTR_BASE + let lookup_z_walk := LOOKUP_Z_COMMS_MPTR_BASE + // lookup 0: 1 helper(s) + 1 acc + // Helper commitments for lookup 0. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 0. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + // lookup 1: 1 helper(s) + 1 acc + // Helper commitments for lookup 1. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 1. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + gas_checkpoint(7) // after lookup helpers + Z accumulators + + // ---- trash_challenge ---- + // Midnight squeezes this challenge unconditionally, even when the + // circuit has no trash arguments. + // Keeping this squeeze unconditional preserves transcript + // compatibility across circuits with and without trash columns. + buf_len := squeeze_to(buf_len, TRASH_CHALLENGE_MPTR) + // ---- trashcans ---- + // Trashcan commitments are optional, but when present they are + // absorbed before y so the quotient batching challenge binds them. + let trashcan_walk := TRASHCAN_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(trashcan_walk, proof_cptr, 0x80) + trashcan_walk := add(trashcan_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(8) // after trash_challenge + trashcans + + // ---- y ---- + // y batches all quotient identities. Quotient commitments are read + // only after y is sampled, matching the Rust verifier flow. + buf_len := squeeze_to(buf_len, Y_MPTR) + + // ---- quotient commitment(s) ---- + // Each uncompressed quotient commitment is calldatacopied directly to + // QUOTIENT_LIMB_COMMS_MPTR_BASE; the Horner fold below reads + // them back from memory. common_uncompressed_g1 absorbs the + // 128-byte calldata form into the transcript verbatim. + // + // Multi-limb quotient mode reads several Q_i commitments; single-H + // mode renders this loop with one limb. + let quotient_walk := QUOTIENT_LIMB_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0200) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(quotient_walk, proof_cptr, 0x80) + quotient_walk := add(quotient_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + gas_checkpoint(9) // after y squeeze + quotient-limb reads + + // ---- x ---- + // x is the main evaluation point. Values read after this point are + // alleged polynomial evaluations at x or derived PCS openings. + buf_len := squeeze_to(buf_len, X_MPTR) + + // ---- evaluations ---- + // Optimisation H3: the off-chain Solidity proof shim rewrites + // proof scalars into BE calldata words. Spill each decoded eval + // into REVERSED_EVALS_MPTR in the same iteration we range-check + // it, so downstream references can use cheap mload. + // + // The Rust verifier conceptually reads evaluations in query order. + // The lowering plan arranges REVERSED_EVALS_MPTR in the order used + // by the quotient VM/direct evaluator, hence the generated name. + { + let eval_buf := REVERSED_EVALS_MPTR + for { let end := add(proof_cptr, 0x0cc0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Proof evaluation scalars must be canonical Fr elements + // before they are absorbed or made available to quotient + // reconstruction. + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } + // Spill for quotient numerator and PCS codegen. + mstore(eval_buf, eval) + eval_buf := add(eval_buf, 0x20) + // Absorb the exact BE field word used by the native + // Keccak transcript. + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + } + + // ---- x1, x2 ---- + // x1 and x2 batch the KZG multi-opening reduction. They are + // squeezed after all polynomial evaluations are absorbed. + buf_len := squeeze_to(buf_len, X1_MPTR) + buf_len := squeeze_to(buf_len, X2_MPTR) + + // ---- f_com (1 uncompressed G1) ---- + // f_com is the commitment to the batched polynomial used by the PCS + // multi-open protocol. It is both transcript material and later + // pairing/MSM input. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(F_COM_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // ---- x3 ---- + // x3 is the PCS evaluation point for f_com. + buf_len := squeeze_to(buf_len, X3_MPTR) + // truncated-challenges mirrors midnight-proofs + // proofs/src/poly/kzg/mod.rs: + // - x3 is the f_com evaluation point and is truncated + // immediately after squeeze. + // - x1 and x4 remain full squeezed Fr words, but later PCS + // batching stores truncate(x1^i) and truncate(x4^i) while + // keeping the internal power accumulators full precision. + // This direct x3 mask is therefore one part of the PCS truncation + // rule, not the only truncated value used by the verifier. + mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) + + // ---- q_evals (one Fq per point set) ---- + // q_evals are not spilled into REVERSED_EVALS_MPTR because the PCS + // emitter reads them as a contiguous calldata range from the saved + // Q_EVAL_CPTR_MPTR cursor. + // + // Each q_eval is the claimed evaluation for one prepared point set + // in the KZG multi-open reduction. They are still transcript + // material and must be range-checked as Fr scalars. + mstore(Q_EVAL_CPTR_MPTR, proof_cptr) + for { let end := add(proof_cptr, 0xa0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Canonical Fr check before transcript absorption. + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + + // ---- x4 ---- + // x4 is the final PCS batching challenge, sampled after q_evals + // and before the opening proof point pi. + buf_len := squeeze_to(buf_len, X4_MPTR) + + // ---- pi (1 uncompressed G1) ---- + // pi is the KZG opening proof commitment. It is the last proof + // object absorbed into the transcript and later becomes one side of + // the final pairing check. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(PI_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // The hand-rolled proof parser must consume exactly the ABI + // `proof` bytes before the `instances` length word. This is + // redundant with the generated proof length today, but makes + // future proof-layout drift fail closed. + // + // NUM_INSTANCE_CPTR is the calldata word immediately after the + // dynamic proof bytes payload. If proof_cptr lands anywhere else, + // some section was under-read or over-read. + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } + + // `success` carries deferred canonicality failures from public + // instance reads. G1/proof scalar helpers revert immediately. + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } + gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) + + // =============================================================== + // Lagrange & instance-evaluation block (pure Fr arithmetic). + // =============================================================== + { + let k := 20 + let x := mload(X_MPTR) + // Compute x^n by repeated squaring, with n = 2^k. + let x_n := x + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_n := mulmod(x_n, x_n, r) + } + + let omega := mload(OMEGA_MPTR) + + // First pass writes denominators (x - omega_i) for every + // Lagrange value needed below, then appends x^n - 1. The + // batch inversion pass turns all of them into inverses in one + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR + let mptr_end := add(mptr, 0x0300) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, addmod(x, sub(r, pow_of_omega), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + let x_n_minus_1 := addmod(x_n, sub(r, 1), r) + mstore(mptr_end, x_n_minus_1) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + + // Convert inverted denominators into Lagrange evaluations: + // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). + mptr := LAGRANGE_DENOMS_MPTR + let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, mulmod(l_i_common, mulmod(mload(mptr), pow_of_omega, r), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + + // l_blind is the sum of the negative-rotation Lagrange terms + // used by the midnight-proofs blinding identity. + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0140) } + lt(l_i_cptr, l_i_cptr_end) + { l_i_cptr := add(l_i_cptr, 0x20) } { + l_blind := addmod(l_blind, mload(l_i_cptr), r) + } + + // Public instance polynomial evaluation at x. Instance words + // have already been range-checked and absorbed in transcript + // order; this loop only forms the linear combination. + let instance_eval := 0 + for { + let instance_cptr := INSTANCE_CPTR + let instance_cptr_end := add(instance_cptr, 0x01c0) + } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) + l_i_cptr := add(l_i_cptr, 0x20) } { + instance_eval := addmod(instance_eval, mulmod(mload(l_i_cptr), calldataload(instance_cptr), r), r) + } + + // Persist the derived values into named memory slots consumed + // by quotient reconstruction and PCS preparation. + let x_n_minus_1_inv := mload(mptr_end) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0140)) + + mstore(X_N_MPTR, x_n) + mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) + mstore(L_LAST_MPTR, l_last) + mstore(L_BLIND_MPTR, l_blind) + mstore(L_0_MPTR, l_0) + mstore(INSTANCE_EVAL_MPTR, instance_eval) + } + gas_checkpoint(11) // after Lagrange + instance evaluation block + + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + + // =============================================================== + // External batched identity numerator reconstruction. + // + // The quotient evaluator receives the verifier memory image from + // QUOTIENT_FRAME_BASE..+QUOTIENT_FRAME_LEN, reconstructs the same + // y-batched numerator, and returns: + // word 0: magic/version + // word 1: linearization expected eval + // word 2..: simple-selector accumulators + // =============================================================== + { + let q_out := QUOTIENT_RETURN_MPTR + // The quotient evaluator is as correctness-critical as the VK: + // it reconstructs the y-batched identity numerator and + // selector buckets. Re-check the pinned runtime before every + // external call, mirroring the VK freshness guard above. + if iszero(and( + eq(extcodesize(quotientEvaluator), EXPECTED_QUOTIENT_LENGTH), + eq(extcodehash(quotientEvaluator), EXPECTED_QUOTIENT_CODEHASH_WORD) + )) { fail(ERR_VK_MISMATCH) } + // gas() forwarding is deliberate here, unlike the precompile + // call sites: this is a regular contract call, so a reverting + // or failing callee refunds its unused gas -- only precompile + // ERRORS burn everything forwarded (EIP-2537). The callee is + // also pinned by codehash above, not attacker-supplied. + if iszero(staticcall(gas(), quotientEvaluator, 0x3680, 0x6ac0, q_out, 0x0180)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + if iszero(eq(returndatasize(), 0x0180)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + if iszero(eq(mload(q_out), 0x00000000000000000000000000000000000000000000000051554556414c0001)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + // Word 1 is the negated y-batched identity numerator, stored + // in the same memory slot used by the monolithic path. + mstore(QUOTIENT_EVAL_MPTR, mload(add(q_out, 0x20))) + // Remaining return words are selector linearization buckets. + // Copy them back into the canonical selector accumulator region + // so the PCS code path is identical for split and monolithic + // quotient renders. + for { let q_i := 0 } lt(q_i, 10) { q_i := add(q_i, 1) } { + mstore(add(SELECTOR_ACC_MPTR, shl(5, q_i)), mload(add(q_out, add(0x40, shl(5, q_i))))) + } + } + gas_checkpoint(12) // after batched identity numerator reconstruction + + // =============================================================== + // Prepare linearization scalars for the final PCS MSM. + // + // The linearized commitment is + // (1 - x^n) * Σ_i x_split^i * Q_i + // + Σ_j sel_acc_j * S_j_com, + // where x_split = x^(n-1). Instead of materializing that point + // with a standalone G1MSM here, PCS block 5 expands the + // linearized commitment into its quotient and selector + // pairs inside the already-fused final MSM. + // + // QUOTIENT_MPTR is no longer a G1 point in this path. Its first + // two words carry: + // word 0: x_split + // word 1: one_minus_x_n + // =============================================================== + { + let x := mload(X_MPTR) + let k := 20 + // Compute both x^n and x^(n-1) with the same squaring walk: + // x_pow_2i tracks x^(2^i), while x_pow_2i_minus1 tracks + // x^(2^i - 1). + let x_pow_2i := x + let x_pow_2i_minus1 := 1 + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_pow_2i_minus1 := mulmod( + mulmod(x_pow_2i_minus1, x_pow_2i_minus1, r), + x, + r + ) + x_pow_2i := mulmod(x_pow_2i, x_pow_2i, r) + } + let x_split := x_pow_2i_minus1 + let one_minus_x_n := addmod(1, sub(r, x_pow_2i), r) + + // PCS block 5 interprets this 2-word payload as scalar + // metadata, not as a materialized G1 point. + mstore(QUOTIENT_MPTR, x_split) + mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) + } + gas_checkpoint(13) // after linearization scalar prep + + // =============================================================== + // PCS computation (multi-prepare emitter from Step 5). + // + // The Rust lowering stage has already expanded the KZG multi-open + // equation into a sequence of generated Yul sub-blocks. Those + // blocks populate: + // - F_EVAL_MPTR / V_MPTR scalar batching values; + // - FINAL_COM_MPTR for the fused commitment MSM; + // - PAIRING_LHS_MPTR and PAIRING_RHS_MPTR for the final pairing. + // =============================================================== + { + // Generated PCS sub-block 1. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // 4 distinct rotation(s) + let x := mload(X_MPTR) + let omega := mload(OMEGA_MPTR) + let omega_inv := mload(OMEGA_INV_MPTR) + let x_pow_of_omega := x + mstore(add(ROT_POINTS_MPTR, 0x40), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega, r) + mstore(add(ROT_POINTS_MPTR, 0x60), x_pow_of_omega) + x_pow_of_omega := x + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x20), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) + } + gas_checkpoint(17) // after PCS sub-block 1 + // Generated PCS sub-block 2. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // pre-compute 43 x1 power(s) + let x1 := mload(X1_MPTR) + mstore(X1_POWERS_MPTR, 1) + let acc := 1 + let p := X1_POWERS_MPTR + for { let i := 0 } lt(i, 0x2a) { i := add(i, 1) } { + p := add(p, 0x20) + acc := mulmod(acc, x1, r) + mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) + } + } + gas_checkpoint(18) // after PCS sub-block 2 + // Generated PCS sub-block 3. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[0]: 43 evaluation term(s), 42 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x9980) + mstore(0xb2a0, 0x9480) + mstore(0xb2c0, 0xa020) + mstore(0xb2e0, 0xa040) + mstore(0xb300, 0xa0a0) + mstore(0xb320, 0xa0c0) + mstore(0xb340, 0xa120) + mstore(0xb360, 0x99a0) + mstore(0xb380, 0x99c0) + mstore(0xb3a0, 0x99e0) + mstore(0xb3c0, 0x9a00) + mstore(0xb3e0, 0x9a20) + mstore(0xb400, 0x9a40) + mstore(0xb420, 0x9a60) + mstore(0xb440, 0x9a80) + mstore(0xb460, 0x9aa0) + mstore(0xb480, 0x9ac0) + mstore(0xb4a0, 0x9ae0) + mstore(0xb4c0, 0x9b00) + mstore(0xb4e0, 0x9b20) + mstore(0xb500, 0x9b40) + mstore(0xb520, 0x9b60) + mstore(0xb540, 0x9b80) + mstore(0xb560, 0x9ba0) + mstore(0xb580, 0x9bc0) + mstore(0xb5a0, 0x9be0) + mstore(0xb5c0, 0x9c00) + mstore(0xb5e0, 0x9c20) + mstore(0xb600, 0x9c40) + mstore(0xb620, 0x9c60) + mstore(0xb640, 0x9c80) + mstore(0xb660, 0x9ca0) + mstore(0xb680, 0x9cc0) + mstore(0xb6a0, 0x9ce0) + mstore(0xb6c0, 0x9d00) + mstore(0xb6e0, 0x9d20) + mstore(0xb700, 0x9d40) + mstore(0xb720, 0x9d60) + mstore(0xb740, 0x9d80) + mstore(0xb760, 0x9da0) + mstore(0xb780, 0x9dc0) + mstore(0xb7a0, 0x9de0) + mstore(0xb7c0, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x9980) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x20) + for { let i := 1 } lt(i, 0x2b) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x20) + } + mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) + } + gas_checkpoint(19) // after PCS sub-block 3 + // Generated PCS sub-block 4. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[1]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9660) + let q_eval_set_1 := mload(0x9920) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x9680), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9940), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x96a0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9960), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) + } + gas_checkpoint(20) // after PCS sub-block 4 + // Generated PCS sub-block 5. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[2]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9fe0) + let q_eval_set_1 := mload(0xa000) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa060), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa080), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa0e0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa100), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) + } + gas_checkpoint(21) // after PCS sub-block 5 + // Generated PCS sub-block 6. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[3]: 11 evaluation term(s), 11 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x94a0) + mstore(0xb2a0, 0x9540) + mstore(0xb2c0, 0x97c0) + mstore(0xb2e0, 0x94c0) + mstore(0xb300, 0x9560) + mstore(0xb320, 0x97e0) + mstore(0xb340, 0x94e0) + mstore(0xb360, 0x9580) + mstore(0xb380, 0x9800) + mstore(0xb3a0, 0x9500) + mstore(0xb3c0, 0x96c0) + mstore(0xb3e0, 0x9820) + mstore(0xb400, 0x9520) + mstore(0xb420, 0x96e0) + mstore(0xb440, 0x9840) + mstore(0xb460, 0x95a0) + mstore(0xb480, 0x9700) + mstore(0xb4a0, 0x9860) + mstore(0xb4c0, 0x95c0) + mstore(0xb4e0, 0x9720) + mstore(0xb500, 0x9880) + mstore(0xb520, 0x95e0) + mstore(0xb540, 0x9740) + mstore(0xb560, 0x98a0) + mstore(0xb580, 0x9600) + mstore(0xb5a0, 0x9760) + mstore(0xb5c0, 0x98c0) + mstore(0xb5e0, 0x9620) + mstore(0xb600, 0x9780) + mstore(0xb620, 0x98e0) + mstore(0xb640, 0x9640) + mstore(0xb660, 0x97a0) + mstore(0xb680, 0x9900) + let q_eval_set_0 := mload(0x94a0) + let q_eval_set_1 := mload(0x9540) + let q_eval_set_2 := mload(0x97c0) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x60) + for { let i := 1 } lt(i, 0xb) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0xa0), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) + } + gas_checkpoint(22) // after PCS sub-block 6 + // Generated PCS sub-block 7. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[4]: 5 evaluation term(s), 5 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x9e00) + mstore(0xb2a0, 0x9e20) + mstore(0xb2c0, 0x9e40) + mstore(0xb2e0, 0x9e60) + mstore(0xb300, 0x9e80) + mstore(0xb320, 0x9ea0) + mstore(0xb340, 0x9ec0) + mstore(0xb360, 0x9ee0) + mstore(0xb380, 0x9f00) + mstore(0xb3a0, 0x9f20) + mstore(0xb3c0, 0x9f40) + mstore(0xb3e0, 0x9f60) + mstore(0xb400, 0x9f80) + mstore(0xb420, 0x9fa0) + mstore(0xb440, 0x9fc0) + let q_eval_set_0 := mload(0x9e00) + let q_eval_set_1 := mload(0x9e20) + let q_eval_set_2 := mload(0x9e40) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x60) + for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0x100), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) + } + gas_checkpoint(23) // after PCS sub-block 7 + // Generated PCS sub-block 8. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // f_eval via Horner over 5 reversed set(s) + let x2 := mload(X2_MPTR) + let x3 := mload(X3_MPTR) + let f_eval := 0 + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let rot_pt_0 := mload(add(ROT_POINTS_MPTR, 0x0)) + let rot_pt_1 := mload(add(ROT_POINTS_MPTR, 0x20)) + let rot_pt_2 := mload(add(ROT_POINTS_MPTR, 0x40)) + let rot_pt_3 := mload(add(ROT_POINTS_MPTR, 0x60)) + // --- set 4 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_0), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_0), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_0), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x100)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x120)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x140)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 3 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_1), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xa0)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xc0)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xe0)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 2 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x60)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x80)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 1 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x20)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x40)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 0 (cardinality 1) --- + { + let dx0 := addmod(x3, sub(r, rot_pt_2), r) + let dx0_inv := scalar_inv(dx0) + let eval := mulmod(addmod(calldataload(add(Q_EVAL_CPTR, 0x0)), sub(r, mload(add(Q_EVAL_SET_MPTR, 0x0))), r), dx0_inv, r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + mstore(F_EVAL_MPTR, f_eval) + } + gas_checkpoint(24) // after PCS sub-block 8 + // Generated PCS sub-block 9. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // build final_com and v (KZG single-opening proof, fused MSM) + // final MSM input length from circuit/VK shape: 78 term(s) + let x4 := mload(X4_MPTR) + let lin_x_split := mload(QUOTIENT_MPTR) + let lin_one_minus_x_n := mload(add(QUOTIENT_MPTR, 0x20)) + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let x4_pow_full := 1 + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_1 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_2 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_3 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_4 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_5 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + let v := calldataload(Q_EVAL_CPTR) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), x4_pow_4, r), r) + v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_5, r), r) + mcopy(0xb280, 0xa840, 0x80) + mstore(0xb300, 1) + mcopy(0xb320, 0xa8c0, 0x80) + mstore(0xb3a0, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0xb3c0, 0xacc0, 0x80) + mstore(0xb440, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0xb460, 0xa940, 0x80) + mstore(0xb4e0, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0xb500, 0xad40, 0x80) + mstore(0xb580, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0xb5a0, 0xaec0, 0x80) + mstore(0xb620, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0xb640, 0x6700, 0x80) + mstore(0xb6c0, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0xb6e0, 0x6480, 0x80) + mstore(0xb760, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0xb780, 0x6500, 0x80) + mstore(0xb800, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0xb820, 0x6580, 0x80) + mstore(0xb8a0, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0xb8c0, 0x6600, 0x80) + mstore(0xb940, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0xb960, 0x6680, 0x80) + mstore(0xb9e0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0xba00, 0x6280, 0x80) + mstore(0xba80, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0xbaa0, 0x6300, 0x80) + mstore(0xbb20, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0xbb40, 0x6380, 0x80) + mstore(0xbbc0, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0xbbe0, 0x6400, 0x80) + mstore(0xbc60, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0xbc80, 0x6780, 0x80) + mstore(0xbd00, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0xbd20, 0x6800, 0x80) + mstore(0xbda0, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0xbdc0, 0x6880, 0x80) + mstore(0xbe40, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0xbe60, 0x6900, 0x80) + mstore(0xbee0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0xbf00, 0x6b00, 0x80) + mstore(0xbf80, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0xbfa0, 0x6c00, 0x80) + mstore(0xc020, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0xc040, 0x6f80, 0x80) + mstore(0xc0c0, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0xc0e0, 0x7000, 0x80) + mstore(0xc160, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0xc180, 0x7080, 0x80) + mstore(0xc200, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0xc220, 0x7100, 0x80) + mstore(0xc2a0, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0xc2c0, 0x7180, 0x80) + mstore(0xc340, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0xc360, 0x7200, 0x80) + mstore(0xc3e0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0xc400, 0x7280, 0x80) + mstore(0xc480, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0xc4a0, 0x7300, 0x80) + mstore(0xc520, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0xc540, 0x7380, 0x80) + mstore(0xc5c0, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0xc5e0, 0x7400, 0x80) + mstore(0xc660, mload(add(X1_POWERS_MPTR, 0x400))) + mcopy(0xc680, 0x7480, 0x80) + mstore(0xc700, mload(add(X1_POWERS_MPTR, 0x420))) + mcopy(0xc720, 0x7500, 0x80) + mstore(0xc7a0, mload(add(X1_POWERS_MPTR, 0x440))) + mcopy(0xc7c0, 0x7580, 0x80) + mstore(0xc840, mload(add(X1_POWERS_MPTR, 0x460))) + mcopy(0xc860, 0x7600, 0x80) + mstore(0xc8e0, mload(add(X1_POWERS_MPTR, 0x480))) + mcopy(0xc900, 0x7680, 0x80) + mstore(0xc980, mload(add(X1_POWERS_MPTR, 0x4a0))) + mcopy(0xc9a0, 0x7700, 0x80) + mstore(0xca20, mload(add(X1_POWERS_MPTR, 0x4c0))) + mcopy(0xca40, 0x7780, 0x80) + mstore(0xcac0, mload(add(X1_POWERS_MPTR, 0x4e0))) + mcopy(0xcae0, 0x7800, 0x80) + mstore(0xcb60, mload(add(X1_POWERS_MPTR, 0x500))) + mcopy(0xcb80, 0x7880, 0x80) + mstore(0xcc00, mload(add(X1_POWERS_MPTR, 0x520))) + let lin_query_scalar_41 := mload(add(X1_POWERS_MPTR, 0x540)) + let lin_cur_scalar_41 := mulmod(lin_query_scalar_41, lin_one_minus_x_n, r) + mcopy(0xcc20, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0xcca0, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xccc0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0xcd40, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xcd60, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0xcde0, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xce00, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0xce80, lin_cur_scalar_41) + mcopy(0xcea0, 0x6980, 0x80) + mstore(0xcf20, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0xcf40, 0x6a00, 0x80) + mstore(0xcfc0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0xcfe0, 0x6a80, 0x80) + mstore(0xd060, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0xd080, 0x6b80, 0x80) + mstore(0xd100, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0xd120, 0x6c80, 0x80) + mstore(0xd1a0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0xd1c0, 0x6d00, 0x80) + mstore(0xd240, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0xd260, 0x6d80, 0x80) + mstore(0xd2e0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0xd300, 0x6e00, 0x80) + mstore(0xd380, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0xd3a0, 0x6e80, 0x80) + mstore(0xd420, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0xd440, 0x6f00, 0x80) + mstore(0xd4c0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0xd4e0, 0xa6c0, 0x80) + mstore(0xd560, x4_pow_1) + mcopy(0xd580, 0xa740, 0x80) + mstore(0xd600, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0xd620, 0xa7c0, 0x80) + mstore(0xd6a0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0xd6c0, 0xac40, 0x80) + mstore(0xd740, x4_pow_2) + mcopy(0xd760, 0xadc0, 0x80) + mstore(0xd7e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0xd800, 0xae40, 0x80) + mstore(0xd880, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0xd8a0, 0xa140, 0x80) + mstore(0xd920, x4_pow_3) + mcopy(0xd940, 0xa1c0, 0x80) + mstore(0xd9c0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xd9e0, 0xa240, 0x80) + mstore(0xda60, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_3, r)) + mcopy(0xda80, 0xa2c0, 0x80) + mstore(0xdb00, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_3, r)) + mcopy(0xdb20, 0xa340, 0x80) + mstore(0xdba0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_3, r)) + mcopy(0xdbc0, 0xa3c0, 0x80) + mstore(0xdc40, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_3, r)) + mcopy(0xdc60, 0xa440, 0x80) + mstore(0xdce0, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_3, r)) + mcopy(0xdd00, 0xa4c0, 0x80) + mstore(0xdd80, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_3, r)) + mcopy(0xdda0, 0xa540, 0x80) + mstore(0xde20, mulmod(mload(add(X1_POWERS_MPTR, 0x100)), x4_pow_3, r)) + mcopy(0xde40, 0xa5c0, 0x80) + mstore(0xdec0, mulmod(mload(add(X1_POWERS_MPTR, 0x120)), x4_pow_3, r)) + mcopy(0xdee0, 0xa640, 0x80) + mstore(0xdf60, mulmod(mload(add(X1_POWERS_MPTR, 0x140)), x4_pow_3, r)) + mcopy(0xdf80, 0xa9c0, 0x80) + mstore(0xe000, x4_pow_4) + mcopy(0xe020, 0xaa40, 0x80) + mstore(0xe0a0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_4, r)) + mcopy(0xe0c0, 0xaac0, 0x80) + mstore(0xe140, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_4, r)) + mcopy(0xe160, 0xab40, 0x80) + mstore(0xe1e0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_4, r)) + mcopy(0xe200, 0xabc0, 0x80) + mstore(0xe280, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_4, r)) + mcopy(0xe2a0, F_COM_MPTR, 0x80) + mstore(0xe320, x4_pow_5) + if success { + // exact EIP-2537 G1MSM cost for 78 pair(s) + success := staticcall(525096, 0x0c, 0xb280, 0x30c0, FINAL_COM_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mstore(V_MPTR, v) + } + gas_checkpoint(25) // after PCS sub-block 9 + // Generated PCS sub-block 10. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // Scale z*pi - vG before the final pairing check + // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) + mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x1080, FINAL_COM_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + if success { + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) + } + } + gas_checkpoint(14) // after PCS computation block (= sub-block 6) + + // Batch the prevalidated public IVC accumulator pairing equation + // into the final KZG pairing. + // + // We do not simply multiply the two pairing equations together: + // two bad equations could cancel. Instead, after all four G1 + // pairing inputs are fixed, derive a verifier-local randomizer + // alpha and check: + // + // e(kzg_rhs + alpha * acc_rhs, G2_BASE) + // * e(kzg_lhs + alpha * acc_lhs, NEG_S_G2_BASE) == 1 + // + // If either original equation is bad, this combined equation + // holds for at most one alpha in Fr. + { + let batch_ptr := 0x1000 + + // Domain || vk_digest || KZG rhs/lhs || accumulator rhs/lhs. + // vk_digest makes alpha's binding to the verifying key local + // instead of transitive-through-the-points (audit I-7). + mstore(batch_ptr, 0x70616972696e672d62617463682d6163632d6b7a670000000000000000) + mstore(add(batch_ptr, 0x20), mload(VK_DIGEST_MPTR)) + mcopy(add(batch_ptr, 0x40), PAIRING_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0xc0), PAIRING_LHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x0140), ACC_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x01c0), ACC_LHS_MPTR, 0x80) + // alpha is Fiat-Shamir over the fully materialized pairing + // inputs. Replace the negligible zero draw with one so the + // accumulator equation cannot be accidentally dropped. + let acc_pair_alpha := mod(keccak256(batch_ptr, 0x0240), r) + if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } + + // PAIRING_RHS_MPTR += alpha * ACC_RHS_MPTR. + // First compute alpha * ACC_RHS with a one-pair G1MSM, then + // add it into the KZG RHS point. + mcopy(batch_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_RHS_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + + // PAIRING_LHS_MPTR += alpha * ACC_LHS_MPTR. + // Mirror the same randomized batching on the KZG LHS point. + mcopy(batch_ptr, ACC_LHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_LHS_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + } + gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) + + // The Yul `ec_pairing` helper checks + // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 + // i.e. e(arg0, [1]_2) = e(arg1, [s]_2). + // + // The KZG pairing identity is + // e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2), + // so arg0 must be (final_com - v*G + x3*pi) and arg1 must be + // pi. The PAIRING_*_MPTR slots store + // PAIRING_LHS_MPTR := pi + // PAIRING_RHS_MPTR := final_com - v*G + x3*pi + // -- the historical "LHS"/"RHS" naming follows the dual MSM + // accumulator (left = pi, right = combined) and *not* the + // pairing argument order. Pass them swapped to ec_pairing. + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + gas_checkpoint(16) // after final ec_pairing + + + + // Success path is terminal. Invalid inputs have already reverted, + // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } + mstore(RETURN_MPTR, 1) + return(RETURN_MPTR, 0x20) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/Halo2VerifyingKey.sol b/proofs/solidity-verifier/fixtures/ivc/Halo2VerifyingKey.sol new file mode 100644 index 000000000..c51a80c36 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/Halo2VerifyingKey.sol @@ -0,0 +1,696 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; + +/// @title Halo2 BLS12-381 verifying-key payload. +/// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +/// @dev Byte 0 is an unconditional INVALID opcode so direct calls cannot execute payload bytes as code. The linked verifier pins the full runtime by length/codehash and copies the payload starting at byte 1. +/// @dev The layout follows the verifier inputs derived from +/// `midfall/proofs/src/plonk/mod.rs::VerifyingKey` and the transcript +/// `vk.hash_into` behavior used by `midfall/proofs/src/plonk/verifier.rs`. +/// +/// Layout (in 32-byte words, big-endian). The header slots are generated from +/// Rust's `VkHeaderLayout`; the byte offsets are absolute from the start of the +/// VK payload, not from byte 0 of the runtime. Runtime byte 0 is the INVALID +/// prefix; the verifier loads the payload via +/// `extcodecopy(vk, VK_MPTR, 0x01, vk_payload_len)` and then references each +/// slot by `VK_MPTR + i`. +/// +/// word 0 : vk_digest (Fq, transcript_repr of the CS) +/// word 1 : num_instances +/// word 2 : k (log2 of the domain size) +/// word 3 : n_inv (1/n in Fr) +/// word 4 : omega (n-th primitive root of unity) +/// word 5 : omega_inv +/// word 6 : omega_inv_to_l (omega_inv ^ |rotation_last|) +/// word 7 : has_accumulator (0 or 1) +/// word 8 : acc_offset (instance index of the accumulator) +/// word 9 : num_acc_limbs +/// word 10 : num_acc_limb_bits +/// word 11..14 : G1_BASE (4 words, EIP-2537 padded) +/// word 15..22 : G2_BASE (8 words, EIP-2537 padded) +/// word 23..30 : NEG_S_G2_BASE (8 words, EIP-2537 padded) +/// word 31..30 + Q_PAYLOAD : quotient VM constants + packed bytecode +/// word 31 + Q_PAYLOAD .. : fixed_comms (4 words each) +/// word 31 + Q_PAYLOAD + 4*N_FIXED .. +/// : permutation_comms (4 words each) +/// +/// Notes: +/// - `extcodehash` of this contract is pinned by the linked verifier via +/// `EXPECTED_VK_CODEHASH`, so any byte tweak is detected at deploy time. +/// - The quotient identity interpreter's static program is stored in this +/// pinned VK runtime. The verifier reads it from memory after `extcodecopy`, +/// avoiding verifier-side PUSH32/mstore immediates while keeping the program +/// covered by `EXPECTED_VK_CODEHASH`. +/// - The midnight-proofs migration bakes the per-lookup chunk counts, trashcan +/// structure, and `num_simple_selectors` into the generated verifier code. +contract Halo2VerifyingKey { + /// @notice Deploy the verifying-key payload as this contract's runtime bytecode. + /// @dev The constructor writes an INVALID byte followed by generated words into memory and returns that prefixed runtime. + /// @dev The transient construction buffer starts at `0x80`, preserving Solidity's reserved memory words. + constructor() { + assembly { + // Runtime layout: + // byte 0 : INVALID, so the payload cannot be executed + // byte 1..end : generated VK payload copied by Halo2Verifier + // + // `runtime` includes the INVALID prefix; `payload` points to word + // zero of the verifier-key data described in the contract NatSpec. + let runtime := 0x80 + let payload := add(runtime, 0x01) + mstore8(runtime, 0xfe) + // Header, base-point, and quotient-program words generated from + // VkPayloadLayout. The inline names on each mstore identify the + // exact slot in the rendered source. + mstore(add(payload, 0x0000), 0x04d431b03dc86a4ddf0aef1f258576d6df8e1b884a7aa7564fea2df1b04a0f86) // vk_digest + mstore(add(payload, 0x0020), 0x000000000000000000000000000000000000000000000000000000000000000e) // num_instances + mstore(add(payload, 0x0040), 0x0000000000000000000000000000000000000000000000000000000000000014) // k + mstore(add(payload, 0x0060), 0x73eda0144f284aae5b6554d46c21576b363d4ec725be2bff1a400fff00001001) // n_inv + mstore(add(payload, 0x0080), 0x03e1c54bcb947035a57a6e07cb98de4a2f69e02d265e09d9fece7e0e39898d4b) // omega + mstore(add(payload, 0x00a0), 0x6c39442eade0092768ac033fa6f608750624a1bb17dbc026ef97c3573a28fc8c) // omega_inv + mstore(add(payload, 0x00c0), 0x2a0ccbaa0613f093f2bb6e97859513f0b613d8587eaa92db9e5604b8d6b68d45) // omega_inv_to_l + mstore(add(payload, 0x00e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // has_accumulator + mstore(add(payload, 0x0100), 0x0000000000000000000000000000000000000000000000000000000000000004) // acc_offset + mstore(add(payload, 0x0120), 0x0000000000000000000000000000000000000000000000000000000000000007) // num_acc_limbs + mstore(add(payload, 0x0140), 0x0000000000000000000000000000000000000000000000000000000000000038) // num_acc_limb_bits + mstore(add(payload, 0x0160), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) // g1_x_hi + mstore(add(payload, 0x0180), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) // g1_x_lo + mstore(add(payload, 0x01a0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) // g1_y_hi + mstore(add(payload, 0x01c0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) // g1_y_lo + mstore(add(payload, 0x01e0), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) // g2_x_c0_hi + mstore(add(payload, 0x0200), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) // g2_x_c0_lo + mstore(add(payload, 0x0220), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) // g2_x_c1_hi + mstore(add(payload, 0x0240), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) // g2_x_c1_lo + mstore(add(payload, 0x0260), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) // g2_y_c0_hi + mstore(add(payload, 0x0280), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) // g2_y_c0_lo + mstore(add(payload, 0x02a0), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) // g2_y_c1_hi + mstore(add(payload, 0x02c0), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) // g2_y_c1_lo + mstore(add(payload, 0x02e0), 0x0000000000000000000000000000000007acb569b3187c0fd1993980aa52a6e9) // neg_s_g2_x_c0_hi + mstore(add(payload, 0x0300), 0xe2080b9697fab96abd5c5f1c3b988256f2d99366f1bbccf13cf0e20702fee18c) // neg_s_g2_x_c0_lo + mstore(add(payload, 0x0320), 0x0000000000000000000000000000000004bbe1a24fcc4f988c6ef268d0c1160e) // neg_s_g2_x_c1_hi + mstore(add(payload, 0x0340), 0xac0a0c4f53d80bd74f3d2e4667be27408625a83825354e27c70859883102eb43) // neg_s_g2_x_c1_lo + mstore(add(payload, 0x0360), 0x00000000000000000000000000000000091f5fc856da557cdb852412d3fd2cef) // neg_s_g2_y_c0_hi + mstore(add(payload, 0x0380), 0x9034c9a66ce38bf356c49f6a012109440035923a9cd6c71ca0c8efa5b6badf52) // neg_s_g2_y_c0_lo + mstore(add(payload, 0x03a0), 0x000000000000000000000000000000000af8fa5434d3fdd8c90fe8e532246c49) // neg_s_g2_y_c1_hi + mstore(add(payload, 0x03c0), 0x9926d8d728ccbb4ac40381158e8da573b4895782bfdb788c8ff40ba22032eab3) // neg_s_g2_y_c1_lo + mstore(add(payload, 0x03e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // quotient_const + mstore(add(payload, 0x0400), 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140) // quotient_const + mstore(add(payload, 0x0420), 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a) // quotient_const + mstore(add(payload, 0x0440), 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db) // quotient_const + mstore(add(payload, 0x0460), 0x00bbe1fbe9ef1e2d62490b03a82bf9ef10f5e9b2323033669cf6c50481f63e05) // quotient_const + mstore(add(payload, 0x0480), 0x0000000000000000000000000000000000000000000000000100000000000000) // quotient_const + mstore(add(payload, 0x04a0), 0x0000000000000000000000000000000000010000000000000000000000000000) // quotient_const + mstore(add(payload, 0x04c0), 0x0000000000000000000000000000000000000000000000000000000400000000) // quotient_const + mstore(add(payload, 0x04e0), 0x0000000000000000000000000000000000000000040000000000000000000000) // quotient_const + mstore(add(payload, 0x0500), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x0520), 0x0000000000000000000000000000000000000000000000100000000000000000) // quotient_const + mstore(add(payload, 0x0540), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000) // quotient_const + mstore(add(payload, 0x0560), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefeffffff00000001) // quotient_const + mstore(add(payload, 0x0580), 0x73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x05a0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffb00000001) // quotient_const + mstore(add(payload, 0x05c0), 0x73eda753299d7d483339d80809a1d80553bda402fbfe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x05e0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffefffff001) // quotient_const + mstore(add(payload, 0x0600), 0x73eda753299d7d483339d80809a1d80553bda402fffe5beeffffffff00000001) // quotient_const + mstore(add(payload, 0x0620), 0x00000000000000000000000000000010ff0726c5de281020ad8016cf6f691213) // quotient_const + mstore(add(payload, 0x0640), 0x0000000000000000000000000000002c64068790f917282347187665718b04c8) // quotient_const + mstore(add(payload, 0x0660), 0x00000000000000000000000000000027241bb5338dce8a77499428839473bf3a) // quotient_const + mstore(add(payload, 0x0680), 0x0000000000000000000000000000002b7c4a26a1c7ae6fc4b499d04e4a463c4b) // quotient_const + mstore(add(payload, 0x06a0), 0x000000000000000000000000000000274bc40fcf526be95333a8c22c79465298) // quotient_const + mstore(add(payload, 0x06c0), 0x0000000000000000000000000000002a5ee6db49930276e2939d1c43ac82f744) // quotient_const + mstore(add(payload, 0x06e0), 0x73eda753299d7d483339d80809a1d7edd77e26c51c38afb5debf8afa00c15cc3) // quotient_const + mstore(add(payload, 0x0700), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000002) // quotient_const + mstore(add(payload, 0x0720), 0x73eda753299d7d483339d80809a1d80553bda402fffe53ebc627fffef6280001) // quotient_const + mstore(add(payload, 0x0740), 0x0000000000000000000001000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0760), 0x0000000100000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0780), 0x6bc66e553973f396854f5626172ba135587d41e37a68209402355093fdcaaf6c) // quotient_const + mstore(add(payload, 0x07a0), 0x63f31e3f446953960c9d6964474300df43ab29179970f642a28e39d6c883c74b) // quotient_const + mstore(add(payload, 0x07c0), 0x73eda753299d7d483339d70809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x07e0), 0x73eda752299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0800), 0x082738fdf02989b1adea81e1f27636cffb40621f85963b6afdcaaf6b02355095) // quotient_const + mstore(add(payload, 0x0820), 0x0ffa8913e53429b2269c6ea3c25ed72610127aeb668d65bc5d71c628377c38b6) // quotient_const + mstore(add(payload, 0x0840), 0x01ec1c0519185dfe86132d479c76d0786e1e037d0b05ca47648a1c5d29492c9b) // quotient_const + mstore(add(payload, 0x0860), 0x1e179025ca2470882b34e63940ccbd7ad9090bf414d43b696e093b5a8782528f) // quotient_const + mstore(add(payload, 0x0880), 0x4298bfee9a84c8ef8e83702075cb1abeb576f146636342e3db9ea6b0a4adf29d) // quotient_const + mstore(add(payload, 0x08a0), 0x3c83b078e9abed278d042acc8f3bd21e228716f04be96af8025da860e2d1bba9) // quotient_const + mstore(add(payload, 0x08c0), 0x427868260f487d1ef07edaadf37f5dbe705bd1318290f2577ae756b009c24f11) // quotient_const + mstore(add(payload, 0x08e0), 0x03020e6a35e595abd22838beeadc45cfcb0545d85ca0ab2c59d44c203fac84a7) // quotient_const + mstore(add(payload, 0x0900), 0x000000000000000000000000000000000000000000000000d201000000010000) // quotient_const + mstore(add(payload, 0x0920), 0x0000000100001b7c3f8d3fe3c5b448f1bdeb2ae34698b72d6ce966fc208c05ed) // quotient_const + mstore(add(payload, 0x0940), 0x73eda753299d7d483339d80809a1d7fd4057a4c12f26d1c1778e3360a6820001) // quotient_const + mstore(add(payload, 0x0960), 0x057797fa7060856f215654ff11006fe0acf6a437e9477bf6f782dfac86f2cf75) // quotient_const + mstore(add(payload, 0x0980), 0x0000000000000000000000000000000000000000000000000000000000000002) // quotient_const + mstore(add(payload, 0x09a0), 0x0000000000000000000000000000000000000000000000000200000000000000) // quotient_const + mstore(add(payload, 0x09c0), 0x0000000000000000000000000000000000020000000000000000000000000000) // quotient_const + mstore(add(payload, 0x09e0), 0x73eda753299d7d483339d80809a1d7e13511a4044eaa5bff4600ffff00005556) // quotient_const + mstore(add(payload, 0x0a00), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0a20), 0x73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb80349) // quotient_const + mstore(add(payload, 0x0a40), 0x73eda753299d7d483339d80809a1d7dbdc391ab4d8ac003bbd48021b82456c9b) // quotient_const + mstore(add(payload, 0x0a60), 0x73eda753299d7d483339d80809a1d7d1448ee5caf5eefcffbc9fabc57759e23f) // quotient_const + mstore(add(payload, 0x0a80), 0x73eda753299d7d483339d80809a1d80418c74cc18bb28443d3978d1fd47ffce1) // quotient_const + mstore(add(payload, 0x0aa0), 0x0000000000000000000000000000006425c019bcda40056233b00000068ff970) // quotient_const + mstore(add(payload, 0x0ac0), 0x00000000000000000000000000000052ef09129c4ea4b786856ffbc6fb7526cc) // quotient_const + mstore(add(payload, 0x0ae0), 0x73eda753299d7d483339d80809a1d7d89a085d30fc8a7106a170ac2377c34ab9) // quotient_const + mstore(add(payload, 0x0b00), 0x73eda753299d7d483339d80809a1d7f8ce65bb936f2d836012914aca65f077e1) // quotient_const + mstore(add(payload, 0x0b20), 0x000000000000000000000000000000681e5d7c70141ebdfe86c0a873114c3b84) // quotient_const + mstore(add(payload, 0x0b40), 0x000000000000000000000000000000297784894e27525bc342b7fde37dba9366) // quotient_const + mstore(add(payload, 0x0b60), 0x0000000000000000000000000000000275ecae82e897af7658d0e5be57000640) // quotient_const + mstore(add(payload, 0x0b80), 0x000000000000000000000000000000013af65741744bd7bb2c6872df2b800320) // quotient_const + mstore(add(payload, 0x0ba0), 0x00000000000000000000000000000059736a8da406e7d5f0bd1ea7b710796a90) // quotient_const + mstore(add(payload, 0x0bc0), 0x0000000000000000000000000000000c8557e86f90d0d89eed6eb5349a0f8820) // quotient_const + mstore(add(payload, 0x0be0), 0x0453ae02a5f228d8f956b5eab4fc92bbeea5eb26b6ae4b42b4fdfcfdf026aa22) // quotient_const + mstore(add(payload, 0x0c00), 0x0000000000000000000000000000000000000000000000000000000800000000) // quotient_const + mstore(add(payload, 0x0c20), 0x0000000000000000000000000000000000000000080000000000000000000000) // quotient_const + mstore(add(payload, 0x0c40), 0x0000000000000000000000000000000000000000000000000000000000002000) // quotient_const + mstore(add(payload, 0x0c60), 0x0000000000000000000000000000000000000000000000200000000000000000) // quotient_const + mstore(add(payload, 0x0c80), 0x73eda753299d7d483339d80809a1d7f454b67d3d21d64bde527fe92f9096edee) // quotient_const + mstore(add(payload, 0x0ca0), 0x73eda753299d7d483339d80809a1d7d8efb71c7206e733dbb8e789998e74fb39) // quotient_const + mstore(add(payload, 0x0cc0), 0x73eda753299d7d483339d80809a1d7de2fa1eecf722fd187b66bd77b6b8c40c7) // quotient_const + mstore(add(payload, 0x0ce0), 0x73eda753299d7d483339d80809a1d7d9d7737d61384fec3a4b662fb0b5b9c3b6) // quotient_const + mstore(add(payload, 0x0d00), 0x73eda753299d7d483339d80809a1d7de07f99433ad9272abcc573dd286b9ad69) // quotient_const + mstore(add(payload, 0x0d20), 0x73eda753299d7d483339d80809a1d7daf4d6c8b96cfbe51c6c62e3bb537d08bd) // quotient_const + mstore(add(payload, 0x0d40), 0x00000000000000000000000000000021fe0e4d8bbc5020415b002d9eded22426) // quotient_const + mstore(add(payload, 0x0d60), 0x00000000000000000000000000000058c80d0f21f22e50468e30eccae3160990) // quotient_const + mstore(add(payload, 0x0d80), 0x0000000000000000000000000000004e48376a671b9d14ee9328510728e77e74) // quotient_const + mstore(add(payload, 0x0da0), 0x00000000000000000000000000000056f8944d438f5cdf896933a09c948c7896) // quotient_const + mstore(add(payload, 0x0dc0), 0x0000000000000000000000000000004e97881f9ea4d7d2a667518458f28ca530) // quotient_const + mstore(add(payload, 0x0de0), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb9194fffd) // quotient_const + mstore(add(payload, 0x0e00), 0x0000000000000000000002000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0e20), 0x0000000200000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0e40), 0x639f3557494a69e4d764d44424b56a655d3cdfc3f4d1e529046aa128fb955ed7) // quotient_const + mstore(add(payload, 0x0e60), 0x53f8952b5f3529e3e600fac084e429b93398ae2c32e39086451c73ae91078e95) // quotient_const + mstore(add(payload, 0x0e80), 0x72018b4e10851f49ad26aac06d2b078ce59fa085f4f891b79b75e3a1d6b6d366) // quotient_const + mstore(add(payload, 0x0ea0), 0x55d6172d5f790cc00804f1cec8d51a8a7ab4980eeb2a209591f6c4a4787dad72) // quotient_const + mstore(add(payload, 0x0ec0), 0x3154e7648f18b458a4b667e793d6bd469e46b2bc9c9b191b2461594e5b520d64) // quotient_const + mstore(add(payload, 0x0ee0), 0x3769f6da3ff19020a635ad3b7a6605e731368d12b414f106fda2579e1d2e4458) // quotient_const + mstore(add(payload, 0x0f00), 0x31753f2d1a55002942bafd5a16227a46e361d2d17d6d69a78518a94ef63db0f0) // quotient_const + mstore(add(payload, 0x0f20), 0x70eb98e8f3b7e79c61119f491ec5923588b85e2aa35db0d2a62bb3dec0537b5a) // quotient_const + mstore(add(payload, 0x0f40), 0x03d8380a3230bbfd0c265a8f38eda0f0dc3c06fa160b948ec91438ba52925936) // quotient_const + mstore(add(payload, 0x0f60), 0x3c2f204b9448e1105669cc7281997af5b21217e829a876d2dc1276b50f04a51e) // quotient_const + mstore(add(payload, 0x0f80), 0x1143d88a0b6c1496e9cd0838e1f45d7817303e89c6c829c8b73d4d62495be539) // quotient_const + mstore(add(payload, 0x0fa0), 0x0519b99ea9ba5d06e6ce7d9114d5cc36f15089dd97d479f104bb50c2c5a37751) // quotient_const + mstore(add(payload, 0x0fc0), 0x110328f8f4f37cf5adc3dd53dd5ce3778cf9fe60052388aff5cead6113849e21) // quotient_const + mstore(add(payload, 0x0fe0), 0x057797fa7060856f215655ff11006fee9a1697597c277945ddaadfac83aad2c0) // quotient_const + mstore(add(payload, 0x1000), 0x0000000000000000000000000000003212e00cde6d2002b119d800000347fcb8) // quotient_const + mstore(add(payload, 0x1020), 0x000000000000000000000000000000340f2ebe380a0f5eff4360543988a61dc2) // quotient_const + mstore(add(payload, 0x1040), 0x0000000000000000000000000000002cb9b546d20373eaf85e8f53db883cb548) // quotient_const + mstore(add(payload, 0x1060), 0x0453ae02a5f228d8f956b6eab50092aaff9ec460d8863b22077de62a80bd8812) // quotient_const + mstore(add(payload, 0x1080), 0x73eda753299d7d4833351088b4af7508df8b737010b26601ef73fcbb87bd0000) // quotient_const + mstore(add(payload, 0x10a0), 0x0aef2ff4e0c10ade42aca9fe2200e00159ed486fd28ef7edef05bf590de59ef3) // quotient_const + mstore(add(payload, 0x10c0), 0x0000000000000000000000000000000000000000000000000000000000000006) // quotient_const + mstore(add(payload, 0x10e0), 0x0000000000000000000000000000000000000000000000000600000000000000) // quotient_const + mstore(add(payload, 0x1100), 0x0000000000000000000000000000000000060000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1120), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff) // quotient_const + mstore(add(payload, 0x1140), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefdffffff00000001) // quotient_const + mstore(add(payload, 0x1160), 0x73eda753299d7d483339d80809a1d80553bba402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1180), 0x0000000000000000000000000000000000000000000000000000000000000003) // quotient_const + mstore(add(payload, 0x11a0), 0x0000000000000000000000000000000000030000000000000000000000000000) // quotient_const + mstore(add(payload, 0x11c0), 0x0000000000000000000000000000012c71404d368ec010269b10000013afec50) // quotient_const + mstore(add(payload, 0x11e0), 0x000000000000000000000000000000f8cd1b37d4ebee2693904ff354f25f7464) // quotient_const + mstore(add(payload, 0x1200), 0x000000000000000000000000000001385b1875503c5c39fb9441f95933e4b28c) // quotient_const + mstore(add(payload, 0x1220), 0x0000000000000000000000000000007c668d9bea75f71349c827f9aa792fba32) // quotient_const + mstore(add(payload, 0x1240), 0x0000000000000000000000000000000761c60b88b9c70e630a72b13b050012c0) // quotient_const + mstore(add(payload, 0x1260), 0x73eda753299d7d483339d80809a1d7a12dfd8a4625be569ccc4ffffef9700691) // quotient_const + mstore(add(payload, 0x1280), 0x73eda753299d7d483339d80809a1d7b264b49166b159a4787a900438048ad935) // quotient_const + mstore(add(payload, 0x12a0), 0x00000000000000000000000000000003b0e305c45ce387318539589d82800960) // quotient_const + mstore(add(payload, 0x12c0), 0x0000000000000000000000000000010c5a3fa8ec14b781d2375bf725316c3fb0) // quotient_const + mstore(add(payload, 0x12e0), 0x000000000000000000000000000000259007b94eb27289dcc84c1f9dce2e9860) // quotient_const + mstore(add(payload, 0x1300), 0x73eda753299d7d483339d80809a1d79d35602792ebdf9e00793f578beeb3c47d) // quotient_const + mstore(add(payload, 0x1320), 0x73eda753299d7d483339d80809a1d802ddd0f5801766ac88a72f1a40a8fff9c1) // quotient_const + mstore(add(payload, 0x1340), 0x73eda753299d7d483339d80809a1d7abe053165ef916860e42e15847ef869571) // quotient_const + mstore(add(payload, 0x1360), 0x73eda753299d7d483339d80809a1d7ec490dd323de5caac125229595cbe0efc1) // quotient_const + mstore(add(payload, 0x1380), 0x08a75c054be451b1f2ad6bd569f92577dd4bd64d6d5c968569fbf9fbe04d544d) // quotient_const + mstore(add(payload, 0x13a0), 0x0000000000000000000000000000000000000000000000000000001800000000) // quotient_const + mstore(add(payload, 0x13c0), 0x0000000000000000000000000000000000000000180000000000000000000000) // quotient_const + mstore(add(payload, 0x13e0), 0x0000000000000000000000000000000000000000000000000000000000006000) // quotient_const + mstore(add(payload, 0x1400), 0x0000000000000000000000000000000000000000000000600000000000000000) // quotient_const + mstore(add(payload, 0x1420), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffff700000001) // quotient_const + mstore(add(payload, 0x1440), 0x73eda753299d7d483339d80809a1d80553bda402f7fe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1460), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffe001) // quotient_const + mstore(add(payload, 0x1480), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bdeffffffff00000001) // quotient_const + mstore(add(payload, 0x14a0), 0x73eda753299d7d483339d80809a1d7e355af567743ae3bbda4ffd260212ddbdb) // quotient_const + mstore(add(payload, 0x14c0), 0x73eda753299d7d483339d80809a1d7ac8bb094e10dd00bb871cf13341ce9f671) // quotient_const + mstore(add(payload, 0x14e0), 0x73eda753299d7d483339d80809a1d7b70b86399be46147106cd7aef7d718818d) // quotient_const + mstore(add(payload, 0x1500), 0x73eda753299d7d483339d80809a1d7ae5b2956bf70a17c7596cc5f626b73876b) // quotient_const + mstore(add(payload, 0x1520), 0x73eda753299d7d483339d80809a1d7b6bc3584645b26895898ae7ba60d735ad1) // quotient_const + mstore(add(payload, 0x1540), 0x73eda753299d7d483339d80809a1d7b095efed6fd9f96e39d8c5c777a6fa1179) // quotient_const + mstore(add(payload, 0x1560), 0x00000000000000000000000000000065fa2ae8a334f060c4110088dc9c766c72) // quotient_const + mstore(add(payload, 0x1580), 0x00000000000000000000000000000000000000000c0000000000000000000000) // quotient_const + mstore(add(payload, 0x15a0), 0x0000000000000000000000000000010a58272d65d68af0d3aa92c660a9421cb0) // quotient_const + mstore(add(payload, 0x15c0), 0x0000000000000000000000000000000000000000000000300000000000000000) // quotient_const + mstore(add(payload, 0x15e0), 0x000000000000000000000000000000ead8a63f3552d73ecbb978f3157ab67b5c) // quotient_const + mstore(add(payload, 0x1600), 0x000000000000000000000000000000852c1396b2eb457869d549633054a10e58) // quotient_const + mstore(add(payload, 0x1620), 0x00000000000000000000000000000104e9bce7caae169e9c3b9ae1d5bda569c2) // quotient_const + mstore(add(payload, 0x1640), 0x0000000000000000000000000000008274de73e5570b4f4e1dcd70eaded2b4e1) // quotient_const + mstore(add(payload, 0x1660), 0x000000000000000000000000000000ebc6985edbee8777f335f48d0ad7a5ef90) // quotient_const + mstore(add(payload, 0x1680), 0x0000000000000000000000000000007f1cb491dcb90764a7bad754cb0588e5cc) // quotient_const + mstore(add(payload, 0x16a0), 0x73eda753299d7d48333049095fbd120c6b5942dd2166802b5297f978232a0002) // quotient_const + mstore(add(payload, 0x16c0), 0x0000000000000000000006000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x16e0), 0x0000000600000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1700), 0x4302515f88a4431e1fbaccbc5adc8f25703b5745de78f77d0d3fe37cf2c01c83) // quotient_const + mstore(add(payload, 0x1720), 0x140e70dbca64831b4b8f40317b68cd20f34ec27e98adf994cf555b0db316abbd) // quotient_const + mstore(add(payload, 0x1740), 0x73eda753299d7d483339d60809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1760), 0x73eda751299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1780), 0x104e71fbe05313635bd503c3e4ec6d9ff680c43f0b2c76d5fb955ed6046aa12a) // quotient_const + mstore(add(payload, 0x17a0), 0x1ff51227ca6853644d38dd4784bdae4c2024f5d6cd1acb78bae38c506ef8716c) // quotient_const + mstore(add(payload, 0x17c0), 0x70156f48f76cc14b27137d78d0b4371477819d08e9f2c77036ebc744ad6da6cb) // quotient_const + mstore(add(payload, 0x17e0), 0x37be870795549c37dcd00b9588085d0fa1ab8c1ad655e52c23ed8949f0fb5ae3) // quotient_const + mstore(add(payload, 0x1800), 0x62a9cec91e3168b1496ccfcf27ad7a8d3c8d65793936323648c2b29cb6a41ac8) // quotient_const + mstore(add(payload, 0x1820), 0x6ed3edb47fe320414c6b5a76f4cc0bce626d1a256829e20dfb44af3c3a5c88b0) // quotient_const + mstore(add(payload, 0x1840), 0x62ea7e5a34aa00528575fab42c44f48dc6c3a5a2fadad34f0a31529dec7b61e0) // quotient_const + mstore(add(payload, 0x1860), 0x6de98a7ebdd251f08ee9668a33e94c65bdb3185246bd05a64c5767be80a6f6b3) // quotient_const + mstore(add(payload, 0x1880), 0x0b88a81e969233f724730fadaac8e2d294b414ee4222bdac5b3caa2ef7b70ba2) // quotient_const + mstore(add(payload, 0x18a0), 0x0000000300000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x18c0), 0x409fb98f933d25e8d0038d4f7b2a98dbc278a3b57cfb0879943764202d0def59) // quotient_const + mstore(add(payload, 0x18e0), 0x43fe0c177a010031bf648c1cc285529323863340cc562ac9e7aaad86598b55df) // quotient_const + mstore(add(payload, 0x1900), 0x33cb899e22443dc4bd6718aaa5dd18684590bb9d54587d5a25b7e826dc13afab) // quotient_const + mstore(add(payload, 0x1920), 0x5a46b0715e6d5198819eb2abc26638708b1b23dc3e7cb23c4a1bb20f9686f7ad) // quotient_const + mstore(add(payload, 0x1940), 0x0f4d2cdbfd2f1714b46b78b33e8164a4d3f19d98c77d6dd30e31f24850ea65f3) // quotient_const + mstore(add(payload, 0x1960), 0x419d6a1793664a2e73d2a85da4119e5513d7a0cde3bde4e90718f923a87532fa) // quotient_const + mstore(add(payload, 0x1980), 0x33097aeadeda76e1094b97fb9816aa66a6edfb200f6a9a0fe16c08233a8dda63) // quotient_const + mstore(add(payload, 0x19a0), 0x09062b3ea1b0c1037678aa3cc094d16f610fd18915e201850d7ce460bf058df5) // quotient_const + mstore(add(payload, 0x19c0), 0x0456a29a706afacf2158850711006fe0acf6a437e9477bf6f782dfac86f2cf7b) // quotient_const + mstore(add(payload, 0x19e0), 0x0397cc06bc030aab970dabe70cd498bbeea8daaea65607bb6a872125fec74a10) // quotient_const + mstore(add(payload, 0x1a00), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb91950003) // quotient_const + mstore(add(payload, 0x1a20), 0x0594e0109a0005958008060d000b0200010594a01194a01194a005950008060d) // quotient_program + mstore(add(payload, 0x1a40), 0x000b0300000594c01194c01194c005952008060d000b0300010594e01194e011) // quotient_program + mstore(add(payload, 0x1a60), 0x94e00595a008060d000b0300011b00001b000105958008109aa00594a01194a0) // quotient_program + mstore(add(payload, 0x1a80), 0x1195000d01060594c01194c01195200d02060594e01194e01195a00d03060d00) // quotient_program + mstore(add(payload, 0x1aa0), 0x0b0300011b000221020403070002000094a00594c00694e00795000895200095) // quotient_program + mstore(add(payload, 0x1ac0), 0x400595600695800995a00a95c00b95e00c96000d96200e96400f966010968011) // quotient_program + mstore(add(payload, 0x1ae0), 0x96a00796c00896e00997000a972094a00095400595600695800796c00896e009) // quotient_program + mstore(add(payload, 0x1b00), 0x97000a972094c00595400695600795800896c00996e00a970012972094e00695) // quotient_program + mstore(add(payload, 0x1b20), 0x400795600895800996c00a96e012970013972095000795400895600995800a96) // quotient_program + mstore(add(payload, 0x1b40), 0xc01296e013970014972095200895400995600a95801296c01396e01497001597) // quotient_program + mstore(add(payload, 0x1b60), 0x2095a00995400a95601295801396c01496e015970016972095c00a9540129560) // quotient_program + mstore(add(payload, 0x1b80), 0x1395801496c01596e01697001797201897401997800b04000121021a03070001) // quotient_program + mstore(add(payload, 0x1ba0), 0x000094a00594c00694e01b95001c95200095400595600695801d95a01e95c00b) // quotient_program + mstore(add(payload, 0x1bc0), 0x95e00c96000d96201f96402096602196802296a01b96c01c96e01d97001e9720) // quotient_program + mstore(add(payload, 0x1be0), 0x94a00095400595600695801b96c01c96e01d97001e972094c00595400695601b) // quotient_program + mstore(add(payload, 0x1c00), 0x95801c96c01d96e01e970023972094e00695401b95601c95801d96c01e96e023) // quotient_program + mstore(add(payload, 0x1c20), 0x970024972095001b95401c95601d95801e96c02396e024970025972095201c95) // quotient_program + mstore(add(payload, 0x1c40), 0x401d95601e95802396c02496e025970026972095a01d95401e95602395802496) // quotient_program + mstore(add(payload, 0x1c60), 0xc02596e026970027972095c01e95402395602495802596c02696e02797002897) // quotient_program + mstore(add(payload, 0x1c80), 0x202997400b0400011b000321022a01000009000995a00a95c00b95e00c96000d) // quotient_program + mstore(add(payload, 0x1ca0), 0x96200e96400f96600094a00594c00694e00795000895201096801196a0189740) // quotient_program + mstore(add(payload, 0x1cc0), 0x1997800b05000121022b01000008001d95a01e95c00b95e00c96000d96201f96) // quotient_program + mstore(add(payload, 0x1ce0), 0x402096600094a00594c00694e01b95001c95202196802296a02997400b050001) // quotient_program + mstore(add(payload, 0x1d00), 0x210397a02c0000000b2b0b94a00c94c00d94e02d95402e95602f95800b95e00c) // quotient_program + mstore(add(payload, 0x1d20), 0x96000d96203097403197600b94a095e00c94a096000d94a096200c94c095e00d) // quotient_program + mstore(add(payload, 0x1d40), 0x94c096003294c096a00d94e095e03294e096803394e096a03295009660339500) // quotient_program + mstore(add(payload, 0x1d60), 0x968034950096a032952096403395209660349520968035952096a00095409540) // quotient_program + mstore(add(payload, 0x1d80), 0x2e954095602f9540958006956095603695609720369580970037958097203295) // quotient_program + mstore(add(payload, 0x1da0), 0xa096203395a096403495a096603595a096803895a096a03295c096003395c096) // quotient_program + mstore(add(payload, 0x1dc0), 0x203495c096403595c096603895c096803995c096a03696c096e03796c097003a) // quotient_program + mstore(add(payload, 0x1de0), 0x96c097203b96e096e03a96e097003c96e097203d970097003e970097203f9720) // quotient_program + mstore(add(payload, 0x1e00), 0x97200d000b060000210397a04003080002150b94a00c94c00d94e00e95000f95) // quotient_program + mstore(add(payload, 0x1e20), 0x202d95402e95602f95801095a01195c00b95e00c96000d96200e96400f966010) // quotient_program + mstore(add(payload, 0x1e40), 0x96801196a04196c04296e043970044972094a00b95e00c96000d96200e96400f) // quotient_program + mstore(add(payload, 0x1e60), 0x96601096801196a094c00c95e00d96000e96200f96401096601196804596a094) // quotient_program + mstore(add(payload, 0x1e80), 0xe00d95e00e96000f96201096401196604596804696a095000e95e00f96001096) // quotient_program + mstore(add(payload, 0x1ea0), 0x201196404596604696804796a095200f95e01096001196204596404696604796) // quotient_program + mstore(add(payload, 0x1ec0), 0x804896a095400095402e95602f95804196c04296e043970044972095a01095e0) // quotient_program + mstore(add(payload, 0x1ee0), 0x1196004596204696404796604896804996a095c01195e0459600469620479640) // quotient_program + mstore(add(payload, 0x1f00), 0x4896604996804a96a01897401997800695609560419560958042956096c04395) // quotient_program + mstore(add(payload, 0x1f20), 0x6096e044956097004b95609720089580958043958096c044958096e04b958097) // quotient_program + mstore(add(payload, 0x1f40), 0x004c958097200a96c096c04b96c096e04c96c097004d96c097201396e096e04d) // quotient_program + mstore(add(payload, 0x1f60), 0x96e097004e96e0972015970097004f9700972017972097200d000b0600012103) // quotient_program + mstore(add(payload, 0x1f80), 0x97a05003080001150b94a00c94c00d94e01f95002095202d95402e95602f9580) // quotient_program + mstore(add(payload, 0x1fa0), 0x2195a02295c00b95e00c96000d96201f96402096602196802296a05196c05296) // quotient_program + mstore(add(payload, 0x1fc0), 0xe053970054972094a00b95e00c96000d96201f96402096602196802296a094c0) // quotient_program + mstore(add(payload, 0x1fe0), 0x0c95e00d96001f96202096402196602296805596a094e00d95e01f9600209620) // quotient_program + mstore(add(payload, 0x2000), 0x2196402296605596805696a095001f95e0209600219620229640559660569680) // quotient_program + mstore(add(payload, 0x2020), 0x5796a095202095e02196002296205596405696605796805896a095400095402e) // quotient_program + mstore(add(payload, 0x2040), 0x95602f95805196c05296e053970054972095a02195e022960055962056964057) // quotient_program + mstore(add(payload, 0x2060), 0x96605896805996a095c02295e05596005696205796405896605996805a96a029) // quotient_program + mstore(add(payload, 0x2080), 0x97400695609560519560958052956096c053956096e054956097005b95609720) // quotient_program + mstore(add(payload, 0x20a0), 0x1c9580958053958096c054958096e05b958097005c958097201e96c096c05b96) // quotient_program + mstore(add(payload, 0x20c0), 0xc096e05c96c097005d96c097202496e096e05d96e097005e96e0972026970097) // quotient_program + mstore(add(payload, 0x20e0), 0x005f9700972028972097200d000b060001210397a0600000000c390b94a00c94) // quotient_program + mstore(add(payload, 0x2100), 0xc00d94e03097403197600097a00097c00597e00698000b98a00c98c00d98e000) // quotient_program + mstore(add(payload, 0x2120), 0x954097c005954097e006954098000b954098a00c954098c00d954098e0059560) // quotient_program + mstore(add(payload, 0x2140), 0x97c006956097e061956098800c956098a00d956098c0329560996006958097c0) // quotient_program + mstore(add(payload, 0x2160), 0x61958098603b958098800d958098a0329580994033958099600095e097a00596) // quotient_program + mstore(add(payload, 0x2180), 0x0097a006962097a06196c098403b96c098606296c098803296c099203396c099) // quotient_program + mstore(add(payload, 0x21a0), 0x403496c099606196e098203b96e098406296e098603d96e098803296e0990033) // quotient_program + mstore(add(payload, 0x21c0), 0x96e099203496e099403596e0996061970098003b9700982062970098403d9700) // quotient_program + mstore(add(payload, 0x21e0), 0x9860639700988032970098e03397009900349700992035970099403897009960) // quotient_program + mstore(add(payload, 0x2200), 0x61972097e03b9720980062972098203d9720984063972098603f972098803297) // quotient_program + mstore(add(payload, 0x2220), 0x2098c033972098e034972099003597209920389720994039972099600d000b07) // quotient_program + mstore(add(payload, 0x2240), 0x0000210397a064020f000a001997800097a00097c00597e00698000798200898) // quotient_program + mstore(add(payload, 0x2260), 0x400998600a98800b98a00c98c00d98e00e99000f992097a00095e00596000696) // quotient_program + mstore(add(payload, 0x2280), 0x200796400896600996800a96a097c00095400595600695800796c00896e00997) // quotient_program + mstore(add(payload, 0x22a0), 0x000a972097e00595400695600795800896c00996e00a97001297209800069540) // quotient_program + mstore(add(payload, 0x22c0), 0x0795600895800996c00a96e012970013972098200795400895600995800a96c0) // quotient_program + mstore(add(payload, 0x22e0), 0x1296e013970014972098400895400995600a95801296c01396e0149700159720) // quotient_program + mstore(add(payload, 0x2300), 0x98600995400a95601295801396c01496e015970016972098800a954012956013) // quotient_program + mstore(add(payload, 0x2320), 0x95801496c01596e016970017972095400b98a00c98c00d98e00e99000f992010) // quotient_program + mstore(add(payload, 0x2340), 0x994011996095600c98a00d98c00e98e00f990010992011994045996095800d98) // quotient_program + mstore(add(payload, 0x2360), 0xa00e98c00f98e010990011992045994046996096c00e98a00f98c01098e01199) // quotient_program + mstore(add(payload, 0x2380), 0x0045992046994047996096e00f98a01098c01198e04599004699204799404899) // quotient_program + mstore(add(payload, 0x23a0), 0x6097001098a01198c04598e046990047992048994049996097201198a04598c0) // quotient_program + mstore(add(payload, 0x23c0), 0x4698e04799004899204999404a99600b94a00c94c00d94e00e95000f95201095) // quotient_program + mstore(add(payload, 0x23e0), 0xa01195c01897401099401199600d000b070001210397a065020f0009000097a0) // quotient_program + mstore(add(payload, 0x2400), 0x0097c00597e00698001b98201c98401d98601e98800b98a00c98c00d98e01f99) // quotient_program + mstore(add(payload, 0x2420), 0x0020992021994097a00095e00596000696201b96401c96601d96801e96a097c0) // quotient_program + mstore(add(payload, 0x2440), 0x0095400595600695801b96c01c96e01d97001e972097e00595400695601b9580) // quotient_program + mstore(add(payload, 0x2460), 0x1c96c01d96e01e970023972098000695401b95601c95801d96c01e96e0239700) // quotient_program + mstore(add(payload, 0x2480), 0x24972098201b95401c95601d95801e96c02396e024970025972098401c95401d) // quotient_program + mstore(add(payload, 0x24a0), 0x95601e95802396c02496e025970026972098601d95401e95602395802496c025) // quotient_program + mstore(add(payload, 0x24c0), 0x96e026970027972098801e95402395602495802596c02696e027970028972095) // quotient_program + mstore(add(payload, 0x24e0), 0x400b98a00c98c00d98e01f990020992021994022996095600c98a00d98c01f98) // quotient_program + mstore(add(payload, 0x2500), 0xe020990021992022994055996095800d98a01f98c02098e02199002299205599) // quotient_program + mstore(add(payload, 0x2520), 0x4056996096c01f98a02098c02198e022990055992056994057996096e02098a0) // quotient_program + mstore(add(payload, 0x2540), 0x2198c02298e055990056992057994058996097002198a02298c05598e0569900) // quotient_program + mstore(add(payload, 0x2560), 0x57992058994059996097202298a05598c05698e05799005899205999405a9960) // quotient_program + mstore(add(payload, 0x2580), 0x0b94a00c94c00d94e01f95002095202195a02295c02997402299600d000b0700) // quotient_program + mstore(add(payload, 0x25a0), 0x01210397a0660000000b2b6794a06894c06994e06a95406b95606c95806a95e0) // quotient_program + mstore(add(payload, 0x25c0), 0x6b96006c96203097403197606d94a094a06894a094c06994a094e06e94c094c0) // quotient_program + mstore(add(payload, 0x25e0), 0x6f94c095c06f94e095a07094e095c06f9500952070950095a071950095c07295) // quotient_program + mstore(add(payload, 0x2600), 0x20952071952095a073952095c06a954095e06b954096006c954096206b956095) // quotient_program + mstore(add(payload, 0x2620), 0xe06c9560960074956096a06c958095e0749580968075958096a07695a095a077) // quotient_program + mstore(add(payload, 0x2640), 0x95a095c07895c095c074960097207496209700759620972074964096e0759640) // quotient_program + mstore(add(payload, 0x2660), 0x9700799640972074966096c075966096e079966097007a9660972075968096c0) // quotient_program + mstore(add(payload, 0x2680), 0x79968096e07a968097007b968097207996a096c07a96a096e07b96a097007c96) // quotient_program + mstore(add(payload, 0x26a0), 0xa097200d000b080000210397a07d03080002156794a06894c06994e07e95007f) // quotient_program + mstore(add(payload, 0x26c0), 0x95206a95406b95606c95808095a08195c06a95e06b96006c9620829640839660) // quotient_program + mstore(add(payload, 0x26e0), 0x8496808596a08296c08396e084970085972094a06d94a06894c06994e07e9500) // quotient_program + mstore(add(payload, 0x2700), 0x7f95208095a08195c095406a95e06b96006c96208296408396608496808596a0) // quotient_program + mstore(add(payload, 0x2720), 0x95606b95e06c96008296208396408496608596808696a095806c95e082960083) // quotient_program + mstore(add(payload, 0x2740), 0x96208496408596608696808796a096c08295e083960084962085964086966087) // quotient_program + mstore(add(payload, 0x2760), 0x96808896a096e08395e08496008596208696408796608896808996a097008495) // quotient_program + mstore(add(payload, 0x2780), 0xe08596008696208796408896608996808a96a097208595e08696008796208896) // quotient_program + mstore(add(payload, 0x27a0), 0x408996608a96808b96a01897401997806e94c094c07e94c094e07f94c0950080) // quotient_program + mstore(add(payload, 0x27c0), 0x94c095208194c095a08c94c095c08d94e094e08094e095008194e095208c94e0) // quotient_program + mstore(add(payload, 0x27e0), 0x95a08e94e095c08f950095008c950095208e950095a090950095c09195209520) // quotient_program + mstore(add(payload, 0x2800), 0x90952095a092952095c09395a095a09495a095c09595c095c00d000b08000121) // quotient_program + mstore(add(payload, 0x2820), 0x0397a09603080001156794a06894c06994e09795009895206a95406b95606c95) // quotient_program + mstore(add(payload, 0x2840), 0x809995a09a95c06a95e06b96006c96209b96409c96609d96809e96a09b96c09c) // quotient_program + mstore(add(payload, 0x2860), 0x96e09d97009e972094a06d94a06894c06994e09795009895209995a09a95c095) // quotient_program + mstore(add(payload, 0x2880), 0x406a95e06b96006c96209b96409c96609d96809e96a095606b95e06c96009b96) // quotient_program + mstore(add(payload, 0x28a0), 0x209c96409d96609e96809f96a095806c95e09b96009c96209d96409e96609f96) // quotient_program + mstore(add(payload, 0x28c0), 0x80a096a096c09b95e09c96009d96209e96409f9660a09680a196a096e09c95e0) // quotient_program + mstore(add(payload, 0x28e0), 0x9d96009e96209f9640a09660a19680a296a097009d95e09e96009f9620a09640) // quotient_program + mstore(add(payload, 0x2900), 0xa19660a29680a396a097209e95e09f9600a09620a19640a29660a39680a496a0) // quotient_program + mstore(add(payload, 0x2920), 0x2997406e94c094c09794c094e09894c095009994c095209a94c095a0a594c095) // quotient_program + mstore(add(payload, 0x2940), 0xc0a694e094e09994e095009a94e09520a594e095a0a794e095c0a895009500a5) // quotient_program + mstore(add(payload, 0x2960), 0x95009520a7950095a0a9950095c0aa95209520a9952095a0ab952095c0ac95a0) // quotient_program + mstore(add(payload, 0x2980), 0x95a0ad95a095c0ae95c095c00d000b080001210397a0af0000000e100094a005) // quotient_program + mstore(add(payload, 0x29a0), 0x94c00694e06a95406b95606c95800095e00596000696203097403197600097c0) // quotient_program + mstore(add(payload, 0x29c0), 0x0597e00698000b954095406b954095606c954095800d95609560749560972074) // quotient_program + mstore(add(payload, 0x29e0), 0x9580970075958097207496c096e07596c097007996c097203396e096e07996e0) // quotient_program + mstore(add(payload, 0x2a00), 0x97007a96e0972035970097007b9700972039972097200d000b090000210397a0) // quotient_program + mstore(add(payload, 0x2a20), 0xb004010002150094a00594c00694e00795000895206a95406b95606c95800995) // quotient_program + mstore(add(payload, 0x2a40), 0xa00a95c00095e00596000696200796400896600996800a96a08296c08396e084) // quotient_program + mstore(add(payload, 0x2a60), 0x97008597200097c00597e00698000798200898400998600a988095400b95406b) // quotient_program + mstore(add(payload, 0x2a80), 0x95606c95808296c08396e08497008597201897401997800d9560956082956095) // quotient_program + mstore(add(payload, 0x2aa0), 0x8083956096c084956096e0859560970086956097200f9580958084958096c085) // quotient_program + mstore(add(payload, 0x2ac0), 0x958096e0869580970087958097201196c096c08696c096e08796c097008896c0) // quotient_program + mstore(add(payload, 0x2ae0), 0x97204696e096e08896e097008996e0972048970097008a970097204a97209720) // quotient_program + mstore(add(payload, 0x2b00), 0x0d000b090001210397a0b104010001150094a00594c00694e01b95001c95206a) // quotient_program + mstore(add(payload, 0x2b20), 0x95406b95606c95801d95a01e95c00095e00596000696201b96401c96601d9680) // quotient_program + mstore(add(payload, 0x2b40), 0x1e96a09b96c09c96e09d97009e97200097c00597e00698001b98201c98401d98) // quotient_program + mstore(add(payload, 0x2b60), 0x601e988095400b95406b95606c95809b96c09c96e09d97009e97202997400d95) // quotient_program + mstore(add(payload, 0x2b80), 0x6095609b956095809c956096c09d956096e09e956097009f9560972020958095) // quotient_program + mstore(add(payload, 0x2ba0), 0x809d958096c09e958096e09f95809700a0958097202296c096c09f96c096e0a0) // quotient_program + mstore(add(payload, 0x2bc0), 0x96c09700a196c097205696e096e0a196e09700a296e097205897009700a39700) // quotient_program + mstore(add(payload, 0x2be0), 0x97205a972097200d000b090001191f0000000000000000000000000000000000) // quotient_program + // Fixed-column commitment 0, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c00), 0x00000000000000000000000000000000055f7961345dce7ce57401dd993cc81a) // fixed_comms[0].x_hi + mstore(add(payload, 0x2c20), 0xb4a8bb072416d10d143dbceefaa489acea245ef19b9b96fef5bf433eb3a11715) // fixed_comms[0].x_lo + mstore(add(payload, 0x2c40), 0x00000000000000000000000000000000123e8a257be057ec25558c37e4b17ce9) // fixed_comms[0].y_hi + mstore(add(payload, 0x2c60), 0x8e3b8e47d06f0961e4194860938f70e8f8f29e6c09dc697cb5c3486879220eaf) // fixed_comms[0].y_lo + // Fixed-column commitment 1, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c80), 0x000000000000000000000000000000000dc17ef381e9c195813396905a4c7619) // fixed_comms[1].x_hi + mstore(add(payload, 0x2ca0), 0x326e62da20d14e245cf995ae353e85749ad599d8859cdf7ff0996b64ad724553) // fixed_comms[1].x_lo + mstore(add(payload, 0x2cc0), 0x0000000000000000000000000000000004af0ea1ccdc1cd0a2a638aa09f6e2ae) // fixed_comms[1].y_hi + mstore(add(payload, 0x2ce0), 0xe2fe34ad7bd96fb709c73d2d9705c120c1c78c8702fc136bcecb58a6efc4353d) // fixed_comms[1].y_lo + // Fixed-column commitment 2, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d00), 0x0000000000000000000000000000000018da87ffd53a1cdfc243a1f594c7db5f) // fixed_comms[2].x_hi + mstore(add(payload, 0x2d20), 0x6a20adfd78c0e9d2dd4a3377189dcaf31f886eecc6bcfd19d7263ff57b36c01e) // fixed_comms[2].x_lo + mstore(add(payload, 0x2d40), 0x0000000000000000000000000000000001921c576e8a2684cc7521fbf6ec96c3) // fixed_comms[2].y_hi + mstore(add(payload, 0x2d60), 0xde86625c4429546e64909b0f415e9b7bcff50f6e96df84f795a6bf7c3fd0e085) // fixed_comms[2].y_lo + // Fixed-column commitment 3, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d80), 0x000000000000000000000000000000001390c4b48f0af2e2a9332b1851fbb5d1) // fixed_comms[3].x_hi + mstore(add(payload, 0x2da0), 0xe13c168e64b12da23a13fb7b449e2f8d38dd385220d9a9cc7af8c1cb5d7e7364) // fixed_comms[3].x_lo + mstore(add(payload, 0x2dc0), 0x0000000000000000000000000000000019b9ac80c33724396a9da36abc8884bc) // fixed_comms[3].y_hi + mstore(add(payload, 0x2de0), 0x9adb1d0bf586080efd6ffd8520aab78d8d205c0a11726db029aecc899111d9a9) // fixed_comms[3].y_lo + // Fixed-column commitment 4, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e00), 0x0000000000000000000000000000000015bc72a82a34331b999a01881b5c4b3b) // fixed_comms[4].x_hi + mstore(add(payload, 0x2e20), 0xb138f213ddd19ae669c32f961811a4164e6eec8fac46a36f5a4e04870f11a3d1) // fixed_comms[4].x_lo + mstore(add(payload, 0x2e40), 0x00000000000000000000000000000000197b5b9237d51d93dc155332b6330653) // fixed_comms[4].y_hi + mstore(add(payload, 0x2e60), 0xef5fa3e86ca7b8acaf6cb32697d8ececfbdac8b2ffe2f53e47bdc8ba9e3993b6) // fixed_comms[4].y_lo + // Fixed-column commitment 5, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e80), 0x0000000000000000000000000000000013eb9d933b5284bfef6caf1f9e08ca59) // fixed_comms[5].x_hi + mstore(add(payload, 0x2ea0), 0x283ea2dbd92fc9a748e3761fff443680718b50347a55be560c0229972f7e0a24) // fixed_comms[5].x_lo + mstore(add(payload, 0x2ec0), 0x000000000000000000000000000000001100235c0764123ef1a72a76e2abc95c) // fixed_comms[5].y_hi + mstore(add(payload, 0x2ee0), 0x8af88c99dda9239bf71fca4d07b5fee527d576ad79c679035b5836347686d74b) // fixed_comms[5].y_lo + // Fixed-column commitment 6, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f00), 0x000000000000000000000000000000000563bb1e4b8d6ac080403aa94fe32b7d) // fixed_comms[6].x_hi + mstore(add(payload, 0x2f20), 0xe1921a07b4a8ade8f03fbf6079d44191250a74ad822758d3f3c76ac1dcf34844) // fixed_comms[6].x_lo + mstore(add(payload, 0x2f40), 0x000000000000000000000000000000001660e1d7dc487ef2074c1cbcc53f96c4) // fixed_comms[6].y_hi + mstore(add(payload, 0x2f60), 0x90a10debafc892aa61fc1f5deccbe9e504f860b20ab154069fc7d9db6abd4b8c) // fixed_comms[6].y_lo + // Fixed-column commitment 7, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f80), 0x0000000000000000000000000000000011d8086a3a74772d6977655ec27ea545) // fixed_comms[7].x_hi + mstore(add(payload, 0x2fa0), 0xb8f93c43434e29d5987c92a14aff1f1eaaf7301c5a7e3089af5e92d1f848d0ca) // fixed_comms[7].x_lo + mstore(add(payload, 0x2fc0), 0x0000000000000000000000000000000005dad0d11cdc8ea706cfc3def7d4b133) // fixed_comms[7].y_hi + mstore(add(payload, 0x2fe0), 0xc3589649c58c36b9396e7ba08d2dcce7e9ffc79c05293e88ffdcd779031bd430) // fixed_comms[7].y_lo + // Fixed-column commitment 8, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3000), 0x000000000000000000000000000000000c93bd7351261d64a616e0136a9d422b) // fixed_comms[8].x_hi + mstore(add(payload, 0x3020), 0xb749e19c58376c59582349ad89c4cd137403a5708d9c57caa9aa60a61ebac5eb) // fixed_comms[8].x_lo + mstore(add(payload, 0x3040), 0x000000000000000000000000000000000cbfb4f76cc2e2dd1cb5c3d5102d3b9a) // fixed_comms[8].y_hi + mstore(add(payload, 0x3060), 0xd4a9cd56cbcb6df99c181927f6448f16319d629b8e456d7c82888b8ebff6605c) // fixed_comms[8].y_lo + // Fixed-column commitment 9, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3080), 0x0000000000000000000000000000000008727a8cb32cb038513549fb17b3ba8d) // fixed_comms[9].x_hi + mstore(add(payload, 0x30a0), 0xd9695316ca607544e71a5430e6910b8fee65f6ad1a50f524c684c5e957c1c73e) // fixed_comms[9].x_lo + mstore(add(payload, 0x30c0), 0x000000000000000000000000000000000108a6377aea32a5e3bbce056526f625) // fixed_comms[9].y_hi + mstore(add(payload, 0x30e0), 0x33a146b087d572d2cdda900fca8baea1863367075e4b606110b6325be4397d52) // fixed_comms[9].y_lo + // Fixed-column commitment 10, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3100), 0x0000000000000000000000000000000004757ce24e0add6ba492d720099e2fbd) // fixed_comms[10].x_hi + mstore(add(payload, 0x3120), 0xc54defd9e931ec5af33e4a92ad1867c2f4d790e30268a97b42e74f65d1d26feb) // fixed_comms[10].x_lo + mstore(add(payload, 0x3140), 0x0000000000000000000000000000000010f63d4681250b4d2f91725c42a7993b) // fixed_comms[10].y_hi + mstore(add(payload, 0x3160), 0xaa140dc9f52cc57f62aa46386e696d66b92a24406de6a8dfb053ebf589cd908b) // fixed_comms[10].y_lo + // Fixed-column commitment 11, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3180), 0x00000000000000000000000000000000085b6410030ecdc020942851047cf237) // fixed_comms[11].x_hi + mstore(add(payload, 0x31a0), 0x9c9c9bb218540241434381109f505f21842aadb79e1290bf15d09779052c830e) // fixed_comms[11].x_lo + mstore(add(payload, 0x31c0), 0x0000000000000000000000000000000001662c17e52c0576a1daf532fd9b5d44) // fixed_comms[11].y_hi + mstore(add(payload, 0x31e0), 0xa3b693810486d8cc2b3231928889a5901b11f8de1ea0a8d7da54719bb1bacb39) // fixed_comms[11].y_lo + // Fixed-column commitment 12, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3200), 0x000000000000000000000000000000001842a8cec63398e3cd72495091717037) // fixed_comms[12].x_hi + mstore(add(payload, 0x3220), 0xed2687fe07e288a66dbddadb866eab042e668795097a24350329af3a2faa15e8) // fixed_comms[12].x_lo + mstore(add(payload, 0x3240), 0x000000000000000000000000000000000cee59973fde1d885353d9f171f17c99) // fixed_comms[12].y_hi + mstore(add(payload, 0x3260), 0xfd88d67302f6cec5cbcd7d8430f17485f2bbaa4a092e725824499b3fc0cf01bd) // fixed_comms[12].y_lo + // Fixed-column commitment 13, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3280), 0x0000000000000000000000000000000019d8a32c4ce7586146ff1ba0ac184d1e) // fixed_comms[13].x_hi + mstore(add(payload, 0x32a0), 0x8263acc4da9e972d5b111879f60990df5070a38e99c93a8e9f52e94ffc5e889e) // fixed_comms[13].x_lo + mstore(add(payload, 0x32c0), 0x00000000000000000000000000000000027e571331ef494a3859d52330b48271) // fixed_comms[13].y_hi + mstore(add(payload, 0x32e0), 0x9386739b55372fd11694634dc2fe32fc5ed8a01fbe87cdbbcfacb4aed4903fc3) // fixed_comms[13].y_lo + // Fixed-column commitment 14, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3300), 0x00000000000000000000000000000000017d226d304f23f7f52ffe5adadcaca9) // fixed_comms[14].x_hi + mstore(add(payload, 0x3320), 0x54a9d37b66adcd661144899193c80d6d365b30bd133f0bd5a60c12da38b4685e) // fixed_comms[14].x_lo + mstore(add(payload, 0x3340), 0x000000000000000000000000000000000f485636ff7bd4a83beb91de87df7ed4) // fixed_comms[14].y_hi + mstore(add(payload, 0x3360), 0x4757ccb9b4c2999112e89a82d9c72579b1d0020cdda10f3e0b41bf57aec34686) // fixed_comms[14].y_lo + // Fixed-column commitment 15, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3380), 0x0000000000000000000000000000000000000000000000000000000000000000) // fixed_comms[15].x_hi + mstore(add(payload, 0x33a0), 0x0000000000000000000000000000000000000000000000000000000000000000) // fixed_comms[15].x_lo + mstore(add(payload, 0x33c0), 0x0000000000000000000000000000000000000000000000000000000000000000) // fixed_comms[15].y_hi + mstore(add(payload, 0x33e0), 0x0000000000000000000000000000000000000000000000000000000000000000) // fixed_comms[15].y_lo + // Fixed-column commitment 16, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3400), 0x0000000000000000000000000000000008fd1060bd58dfc0e828393d62e3aee0) // fixed_comms[16].x_hi + mstore(add(payload, 0x3420), 0xfe8983d71d04414402f77df4de23f2a73b3c5c48ee85fa51fcd64a3027e0167b) // fixed_comms[16].x_lo + mstore(add(payload, 0x3440), 0x000000000000000000000000000000001024eab25af80d874e553df0f690d9e3) // fixed_comms[16].y_hi + mstore(add(payload, 0x3460), 0x634a41f5518caaaeee1dbc993ced1264ade8431a680d202247d83eb8ef1ed620) // fixed_comms[16].y_lo + // Fixed-column commitment 17, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3480), 0x000000000000000000000000000000000b877a879e46bc947071a221c60797d8) // fixed_comms[17].x_hi + mstore(add(payload, 0x34a0), 0x0d338c66213b76689a66c79be8734280510aa45f8c79ce0eaaba3d7f2c201c99) // fixed_comms[17].x_lo + mstore(add(payload, 0x34c0), 0x000000000000000000000000000000000ffcad707a79c0b29c100d2ab1b60935) // fixed_comms[17].y_hi + mstore(add(payload, 0x34e0), 0x2a2c605009875c39b73bb6d3e2216b305369028923566fc6ef2d22c8b74ce2bf) // fixed_comms[17].y_lo + // Fixed-column commitment 18, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3500), 0x000000000000000000000000000000000d0d0b7c841523297c824431ffca522d) // fixed_comms[18].x_hi + mstore(add(payload, 0x3520), 0xb24daa6cbd8c5f783501a4681bf7367e7aa5d71fca25234cd1a8f31ff24b9871) // fixed_comms[18].x_lo + mstore(add(payload, 0x3540), 0x000000000000000000000000000000001396209d456313b44ec6d4a2fe5f434f) // fixed_comms[18].y_hi + mstore(add(payload, 0x3560), 0xd5bd61218a04eaee084c376ade74d9bceba17fb9e12c2341581e5279596d7c97) // fixed_comms[18].y_lo + // Fixed-column commitment 19, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3580), 0x00000000000000000000000000000000136cd0a2afc84eaecd680d24627d704f) // fixed_comms[19].x_hi + mstore(add(payload, 0x35a0), 0x7e67e5d7fa3094f1aac03b8600277d394817f19fd0b0810dd2087b359429bf11) // fixed_comms[19].x_lo + mstore(add(payload, 0x35c0), 0x0000000000000000000000000000000015cedd2e0ff3e776d58981915174cb65) // fixed_comms[19].y_hi + mstore(add(payload, 0x35e0), 0x13bbf7d0f959d6be24d858ac4ae78c6b9ba0c80d69183a84cd73ec4e47693a14) // fixed_comms[19].y_lo + // Fixed-column commitment 20, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3600), 0x0000000000000000000000000000000010e71146f473749481916e34fff2ba4a) // fixed_comms[20].x_hi + mstore(add(payload, 0x3620), 0xd3e26bb933c011913f506f7892a36f43f7ba4f88b3f1c632e707bbffede7feb9) // fixed_comms[20].x_lo + mstore(add(payload, 0x3640), 0x000000000000000000000000000000000982bd3e7a5ba58d468d0835936ad925) // fixed_comms[20].y_hi + mstore(add(payload, 0x3660), 0xa3d7160336e586296b5e3742251d002e79d8d8d409bf94bafd09719f3ff0b382) // fixed_comms[20].y_lo + // Fixed-column commitment 21, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3680), 0x00000000000000000000000000000000141c94b0741a3d6471b0dd59b3420d9e) // fixed_comms[21].x_hi + mstore(add(payload, 0x36a0), 0xa03be602a9ebe7f1567966db7e3ace5a14ae393194015380293d1d995596651c) // fixed_comms[21].x_lo + mstore(add(payload, 0x36c0), 0x000000000000000000000000000000000185758fd177d9c06fad9502b24ca417) // fixed_comms[21].y_hi + mstore(add(payload, 0x36e0), 0x9cbb2dd41d7ede5248fb78316c557683f183c083b731a261a7381d45cf930742) // fixed_comms[21].y_lo + // Fixed-column commitment 22, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3700), 0x0000000000000000000000000000000001e93db65a35232bfd9581766d6c5c59) // fixed_comms[22].x_hi + mstore(add(payload, 0x3720), 0xa90a6d1906f94fc5643024253805f7a17b6fc2428bbe864add9dd8124518153c) // fixed_comms[22].x_lo + mstore(add(payload, 0x3740), 0x000000000000000000000000000000001897a8f562cb20282c670eecf5f77249) // fixed_comms[22].y_hi + mstore(add(payload, 0x3760), 0x4df0f2823207b6a3832a1b0d7987b20827e8416b563efbbfe212f510acf289df) // fixed_comms[22].y_lo + // Fixed-column commitment 23, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3780), 0x000000000000000000000000000000001141339a865a8fc5477605fae30566b9) // fixed_comms[23].x_hi + mstore(add(payload, 0x37a0), 0x7bd7c27d8d218efa4eb28c80916844e8ec5d47c7dc2f871545707f004b7bde50) // fixed_comms[23].x_lo + mstore(add(payload, 0x37c0), 0x000000000000000000000000000000000a728fcbfa5a7a5bfae84b4ab18b83e6) // fixed_comms[23].y_hi + mstore(add(payload, 0x37e0), 0xb10f17f88be257afb9d981d6516cfd2c4b3e89f5fa5ca13434fd95b24bc0f5c1) // fixed_comms[23].y_lo + // Fixed-column commitment 24, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3800), 0x0000000000000000000000000000000002a788024d9035e7e40f601bb8082ff8) // fixed_comms[24].x_hi + mstore(add(payload, 0x3820), 0x53ec39d899cd245ad2ee9ad32549a184d0c5cb4d4d99940a1cda52c4aa50f79d) // fixed_comms[24].x_lo + mstore(add(payload, 0x3840), 0x0000000000000000000000000000000019a91d8fc2a4d3db45f22db05108197e) // fixed_comms[24].y_hi + mstore(add(payload, 0x3860), 0x7eac8129c7add70a75761fd1e18fa2f40ea6e687a3f43b41995a2e74bf31ff24) // fixed_comms[24].y_lo + // Fixed-column commitment 25, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3880), 0x0000000000000000000000000000000001442c60b19c670229debddb76233f20) // fixed_comms[25].x_hi + mstore(add(payload, 0x38a0), 0x6a6627da0dc462833c112cdb1784e1f5f8a9aec196cf07b4442d408dbdc8de34) // fixed_comms[25].x_lo + mstore(add(payload, 0x38c0), 0x0000000000000000000000000000000013f3b855645d02a97df448668efd5d7f) // fixed_comms[25].y_hi + mstore(add(payload, 0x38e0), 0x8d52d302c0337d44e292fff6ae85d5a6dc5a59067e2a4f0925f48080dc521d40) // fixed_comms[25].y_lo + // Fixed-column commitment 26, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3900), 0x00000000000000000000000000000000190429c7a977675dbb8d96b442fe3eb0) // fixed_comms[26].x_hi + mstore(add(payload, 0x3920), 0xb466636ed724932185b8820e3f115e95ce19cde31f721358e32beda77fa44c98) // fixed_comms[26].x_lo + mstore(add(payload, 0x3940), 0x00000000000000000000000000000000058d18d63ff3a3abf337b6edc6a7709a) // fixed_comms[26].y_hi + mstore(add(payload, 0x3960), 0x36a019565bfbe01076597121c5484c2f3c63a301ed971058d75bb5d61b50bf05) // fixed_comms[26].y_lo + // Permutation commitment 0, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3980), 0x00000000000000000000000000000000120ea7ffaddae135109dc68a0169e39e) // permutation_comms[0].x_hi + mstore(add(payload, 0x39a0), 0x17f95865ea6411611a36d6ee8affde13b3120990c5feff3a2dc32b1b71606512) // permutation_comms[0].x_lo + mstore(add(payload, 0x39c0), 0x000000000000000000000000000000000d8604c2a5312ba81e5c56ad904e3fd3) // permutation_comms[0].y_hi + mstore(add(payload, 0x39e0), 0xc73a292e67febcd994e3d882c050e50940bcd0364b49eaf0ea8c838123757e51) // permutation_comms[0].y_lo + // Permutation commitment 1, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a00), 0x0000000000000000000000000000000012baec9d926370da0174d2da9125909f) // permutation_comms[1].x_hi + mstore(add(payload, 0x3a20), 0x51c030289c7267c9173faacb20c43e027ba889a257fc9da7685fe92a1d8e1159) // permutation_comms[1].x_lo + mstore(add(payload, 0x3a40), 0x00000000000000000000000000000000151c6bdabf6387e09e4ab927a972d0a5) // permutation_comms[1].y_hi + mstore(add(payload, 0x3a60), 0x93e74bf9513d1d9be31b2d8828eb248d8b76fd529ee3d3343629b1327f47b74c) // permutation_comms[1].y_lo + // Permutation commitment 2, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a80), 0x000000000000000000000000000000000502f4f83331ce4fe3b1e618b27af0d4) // permutation_comms[2].x_hi + mstore(add(payload, 0x3aa0), 0xed1c9014937f4e0c989f42c2a22d0d54e395b1e6d35830f1681e7f7633bd5e48) // permutation_comms[2].x_lo + mstore(add(payload, 0x3ac0), 0x00000000000000000000000000000000158fff4ecfaf728c449c4f9955fe87b1) // permutation_comms[2].y_hi + mstore(add(payload, 0x3ae0), 0x8938d2b58f9f4325ad6c56ef82526e5d6bedff11dd4ef4730c55430e7ecc12b7) // permutation_comms[2].y_lo + // Permutation commitment 3, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b00), 0x00000000000000000000000000000000157dbf7d9e1605bb29df570e4e4165d2) // permutation_comms[3].x_hi + mstore(add(payload, 0x3b20), 0xe3af202d72afd27c2516ad10f0ef973f33dddb0fb0e34df741b4385b713cd1a8) // permutation_comms[3].x_lo + mstore(add(payload, 0x3b40), 0x000000000000000000000000000000001227de928658870ba5ecaeae8dc6272e) // permutation_comms[3].y_hi + mstore(add(payload, 0x3b60), 0x3c0f65f9d0e0daf9bcae0c8d2f61f4dfe5603cd4f9c2efe24c8bb68df6091f8b) // permutation_comms[3].y_lo + // Permutation commitment 4, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b80), 0x00000000000000000000000000000000095679f4757e1699305ce877a6cede75) // permutation_comms[4].x_hi + mstore(add(payload, 0x3ba0), 0xa7e634400e785e5c802142e6df51aa4b9d707d525cf7a80209699c36020b7971) // permutation_comms[4].x_lo + mstore(add(payload, 0x3bc0), 0x00000000000000000000000000000000044f9e79c7622ffb279d558f84e0e7ec) // permutation_comms[4].y_hi + mstore(add(payload, 0x3be0), 0x8355f52668f5832af0d5f80a018da5d9cf622b4ad414ef10eaad16f94ddc8def) // permutation_comms[4].y_lo + // Permutation commitment 5, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c00), 0x000000000000000000000000000000000dc0d3a9a27cacb14bf99d6c879fc0f2) // permutation_comms[5].x_hi + mstore(add(payload, 0x3c20), 0x92f80d3f8becaa901863aa7ca5b046bbe0f61b5da880eb4b1bb5b40ecf735d54) // permutation_comms[5].x_lo + mstore(add(payload, 0x3c40), 0x00000000000000000000000000000000077a8a36b30f3ba3d444bc0427088640) // permutation_comms[5].y_hi + mstore(add(payload, 0x3c60), 0x8840d2e5d575557d7495f63d40abf4b7daa04fc2bd662f6e299fc27aca5de4c5) // permutation_comms[5].y_lo + // Permutation commitment 6, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c80), 0x0000000000000000000000000000000000986f7215a9b5e4bc69608acaeb755c) // permutation_comms[6].x_hi + mstore(add(payload, 0x3ca0), 0xc0ed4dd7bd88976da07e9c47756f1c9b90ef494c5cc3031ac6c9b5570fe5c45d) // permutation_comms[6].x_lo + mstore(add(payload, 0x3cc0), 0x0000000000000000000000000000000003658180fe0ac3ab217301cc34d2f9aa) // permutation_comms[6].y_hi + mstore(add(payload, 0x3ce0), 0x04b19a3be7154ad43d1737fd1668783a7069648dfb24de83a6b13d7261001032) // permutation_comms[6].y_lo + // Permutation commitment 7, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d00), 0x0000000000000000000000000000000001a53f321f3bf6d20af84c660efc018e) // permutation_comms[7].x_hi + mstore(add(payload, 0x3d20), 0xa56fe987917489d477e3f2ac4da73c5a1e032748de091337870f0d5405473cd7) // permutation_comms[7].x_lo + mstore(add(payload, 0x3d40), 0x000000000000000000000000000000000cc1da20073f989dd769a6d1003df07d) // permutation_comms[7].y_hi + mstore(add(payload, 0x3d60), 0x5f354c49e670c02fcd04d732bf1dcf1ea91bf6fda6ccaf7b3884498827925ad7) // permutation_comms[7].y_lo + // Permutation commitment 8, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d80), 0x0000000000000000000000000000000006416291ebc77017412205eb4523f2c4) // permutation_comms[8].x_hi + mstore(add(payload, 0x3da0), 0xfd559ac96201738085b756438d69e28d22d12d42527f26d4d076768379361baf) // permutation_comms[8].x_lo + mstore(add(payload, 0x3dc0), 0x000000000000000000000000000000000d7c5e9e2123f1a9259e09115e8bcbde) // permutation_comms[8].y_hi + mstore(add(payload, 0x3de0), 0x993cb20b6226d0b408d9ba2ea78f60939c10a918d0b706c0069e74ba1eeb7587) // permutation_comms[8].y_lo + // Permutation commitment 9, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e00), 0x00000000000000000000000000000000155b9dd46792d67982688c923afe6f78) // permutation_comms[9].x_hi + mstore(add(payload, 0x3e20), 0xf0a9671faf2fbd90216a37854e9cc8f38a4e4e2edfbec79eabb17473cb14233a) // permutation_comms[9].x_lo + mstore(add(payload, 0x3e40), 0x000000000000000000000000000000001136f0b77aaf0d619c1ab6b1f0dcee68) // permutation_comms[9].y_hi + mstore(add(payload, 0x3e60), 0x5a362fa96be37353acf9f0ad8061c2da4ea54d0a983b25c140fff64eb5814a43) // permutation_comms[9].y_lo + // Permutation commitment 10, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e80), 0x00000000000000000000000000000000190c34b5f99861f6dfcdde40fbfd95d0) // permutation_comms[10].x_hi + mstore(add(payload, 0x3ea0), 0x95a6bd47b58b054fe90452e3b358cd4dba6dd76fa6d5877d1ad1a6dc5f2c2ad7) // permutation_comms[10].x_lo + mstore(add(payload, 0x3ec0), 0x00000000000000000000000000000000025a2fa63f92a2b0012325d053fb4dd7) // permutation_comms[10].y_hi + mstore(add(payload, 0x3ee0), 0xeea7e8f98e3d57f1d404d8c6266072c77e1bf610f1b2d7e66a24c4eb5c7fa858) // permutation_comms[10].y_lo + // Permutation commitment 11, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f00), 0x0000000000000000000000000000000006dba7402f78627c2b84e3f197be1ff9) // permutation_comms[11].x_hi + mstore(add(payload, 0x3f20), 0xe3b778c0de9938ede71f40c70a82c4b21e7f7e17f5ae37d8f431d8c56cb0cb7d) // permutation_comms[11].x_lo + mstore(add(payload, 0x3f40), 0x0000000000000000000000000000000002f707f86413969433104530051e0e7f) // permutation_comms[11].y_hi + mstore(add(payload, 0x3f60), 0xef774edc804973bdd3807446ef32363e657593d67b0460a33e76fc23d9feb998) // permutation_comms[11].y_lo + // Permutation commitment 12, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f80), 0x00000000000000000000000000000000116f8891da6e5ddfca44225df76baa02) // permutation_comms[12].x_hi + mstore(add(payload, 0x3fa0), 0x795f3f817116f937e2102b4196eb86112a0d3290a6811fa1e7ada37bdcd91129) // permutation_comms[12].x_lo + mstore(add(payload, 0x3fc0), 0x0000000000000000000000000000000012b7e92df964086ecd115e6b47daad77) // permutation_comms[12].y_hi + mstore(add(payload, 0x3fe0), 0xcf396451a41cf55fa02fe68aaea4416953ad112a6608797bcc1f4a06ec6a786f) // permutation_comms[12].y_lo + // Permutation commitment 13, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4000), 0x000000000000000000000000000000000c92a9011db64f5f7857340d5ecc0e76) // permutation_comms[13].x_hi + mstore(add(payload, 0x4020), 0x6d6a2f4362053c3253503813a1a4167c6848cdbae2c90b104fc2a05290a24f78) // permutation_comms[13].x_lo + mstore(add(payload, 0x4040), 0x00000000000000000000000000000000143212478a1a01c12e08520644d09291) // permutation_comms[13].y_hi + mstore(add(payload, 0x4060), 0x4b417957d5c80179f30c06b68b329aa111de3f613cc66c70fd4841ec26139999) // permutation_comms[13].y_lo + // Permutation commitment 14, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4080), 0x000000000000000000000000000000000a03f34520ffafc5b6a3f90f70e4e373) // permutation_comms[14].x_hi + mstore(add(payload, 0x40a0), 0x673b3105b9864666ae11b398378b0648279065fc693c5edc4d42da133102545c) // permutation_comms[14].x_lo + mstore(add(payload, 0x40c0), 0x00000000000000000000000000000000073205a58fda6d5adfd59e82867a548c) // permutation_comms[14].y_hi + mstore(add(payload, 0x40e0), 0x80b028b16770d9a79d949f623d043d9aeff1cac440bd1482d1c898d7fc7bbbce) // permutation_comms[14].y_lo + // Permutation commitment 15, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4100), 0x0000000000000000000000000000000008f70210c58006a3cb8b927287fa0bfb) // permutation_comms[15].x_hi + mstore(add(payload, 0x4120), 0x55da954a29534ecb479d764e8ca977440dee39b3d09ebbe6b3fbb9c5b92cfe8b) // permutation_comms[15].x_lo + mstore(add(payload, 0x4140), 0x0000000000000000000000000000000007285c63d0eadb56452c598285dfaa89) // permutation_comms[15].y_hi + mstore(add(payload, 0x4160), 0x7d569c04ddbe85aa5deb978df472a328eb6db26586db41278f2cddeee55568cf) // permutation_comms[15].y_lo + // Permutation commitment 16, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4180), 0x0000000000000000000000000000000019e4494289915c32cc82722cdf9cd7f2) // permutation_comms[16].x_hi + mstore(add(payload, 0x41a0), 0xdc0187db7a50b887b1b5b2003c62effcf00f99c2a920ec56cfee0462655ac108) // permutation_comms[16].x_lo + mstore(add(payload, 0x41c0), 0x000000000000000000000000000000001933cb285d7f70cc217fdf83c792d776) // permutation_comms[16].y_hi + mstore(add(payload, 0x41e0), 0x527c8109795e8922038ac6a98e67c92d0fcb6e3a700e7ec2c0fa98cc1df52702) // permutation_comms[16].y_lo + // Permutation commitment 17, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4200), 0x000000000000000000000000000000000485fd87bac44c42dc3c80fd042cc7d0) // permutation_comms[17].x_hi + mstore(add(payload, 0x4220), 0xbb99316753b744e151e09c7f07e670300181f2f8e79a8b37c9828ac6be7a9573) // permutation_comms[17].x_lo + mstore(add(payload, 0x4240), 0x000000000000000000000000000000000fd370bb45717fa6282230adcff6e4b6) // permutation_comms[17].y_hi + mstore(add(payload, 0x4260), 0xab0ead4db625a61209a55551a96a3b5618f2996e645f09bbfda12ca3881536ac) // permutation_comms[17].y_lo + + // Return exactly the INVALID prefix plus the generated payload. The + // linked verifier pins this byte length and the resulting codehash. + return(runtime, 0x4281) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/README.md b/proofs/solidity-verifier/fixtures/ivc/README.md new file mode 100644 index 000000000..8ff87be09 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/ivc/README.md @@ -0,0 +1,59 @@ +# IVC Public-Accumulator Replay Fixture + +> **STALE — regeneration required before any deployment.** These artifacts were +> rendered before the MF-1 fix (`MODEXP_GAS` raised to the EIP-7883 bound and a +> constructor modexp known-answer probe added), so the committed `.sol` files +> here still carry the old 1360 bound and no modexp probe. They remain valid +> inputs for the *replay* tests, which exercise verification logic rather than +> the modexp bound, but they must be regenerated (and their provenance rows +> below updated) on a host with the pinned solc before they are used as a +> deployment source. Regeneration needs the SRS asset +> (`zk_stdlib/examples/assets/bls_filecoin_2p19`) and a full proving run, which +> the environment that applied the MF-1 fix could not reach. + +Pre-rendered artifacts for `tests/ivc_accumulator_replay.rs`, which replays a +real IVC final proof and then mutates the accumulator public inputs to check the +decoder in `templates/partials/verifier/AccumulatorHelpers.yul` rejects them. + +These are *rendered* contracts plus matching calldata rather than a verifying +key and proof, so the replay needs only solc and revm -- no SRS, no proving run, +and no `midnight-aggregation`. A verifier cannot be rendered from a VK without +the full SRS, because `SolidityGenerator` consumes `params.g_lagrange()`; that +is what makes a vk.bin-based replay unusable in CI. + +## Provenance + +| Field | Value | +| --- | --- | +| Source commit | `f894f75` | +| Rendered by | `tests/ivc_keccak_solidity.rs` (`ivc_final_keccak_solidity_e2e`) | +| Circuit | IVC k=19 leaves, k=20 decider | +| Accumulator | `AccumulatorEncoding::new(offset=4, num_limbs=7, num_limb_bits=56)` | +| Verified on-chain | yes, 1,365,883 gas under revm Prague (2026-08-13 render: exact precompile gas bounds, typed errors, VM operand clamps, BUILD_ID, alpha vk-binding; gas-checkpoint bench profile) | + +## Regenerating + +Requires `midnight-srs-2p19` and `midnight-srs-2p20` in `SRS_DIR` +(~300 MB, from ): + +```bash +HALO2_SOLIDITY_RUN_IVC_BENCH=1 \ + SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ + cargo test --release \ + --features evm,truncated-challenges,in-circuit-fewer-point-sets \ + --test ivc_keccak_solidity -- --nocapture + +cp target/ivc-keccak-solidity-dump/{Halo2Verifier.sol,Halo2VerifyingKey.sol,\ +Halo2QuotientEvaluator.sol,calldata.bin} fixtures/ivc/ +``` + +Then update the source commit above. + +## Staleness + +This is a snapshot of the codegen that produced it. The artifacts are +self-consistent, so the replay keeps passing after a codegen change -- it just +stops exercising current output. Detecting that automatically would mean +re-rendering, which needs the SRS again, so it is tracked by the commit stamp +above rather than by an assertion. Regenerate after changes to the accumulator +templates or the memory layout. diff --git a/proofs/solidity-verifier/fixtures/ivc/calldata.bin b/proofs/solidity-verifier/fixtures/ivc/calldata.bin new file mode 100644 index 000000000..0e7e61f5b Binary files /dev/null and b/proofs/solidity-verifier/fixtures/ivc/calldata.bin differ diff --git a/proofs/solidity-verifier/fixtures/ivc/instance.bin b/proofs/solidity-verifier/fixtures/ivc/instance.bin deleted file mode 100644 index 9247d55a5..000000000 Binary files a/proofs/solidity-verifier/fixtures/ivc/instance.bin and /dev/null differ diff --git a/proofs/solidity-verifier/fixtures/ivc/proof.bin b/proofs/solidity-verifier/fixtures/ivc/proof.bin deleted file mode 100644 index 910bd9d65..000000000 Binary files a/proofs/solidity-verifier/fixtures/ivc/proof.bin and /dev/null differ diff --git a/proofs/solidity-verifier/fixtures/ivc/rust_trace.json b/proofs/solidity-verifier/fixtures/ivc/rust_trace.json deleted file mode 100644 index 3f5fe0189..000000000 --- a/proofs/solidity-verifier/fixtures/ivc/rust_trace.json +++ /dev/null @@ -1,1068 +0,0 @@ -{ - "entries": [ - { - "kind": "Intermediate", - "data": { - "tag": "vk_repr", - "fe_be_hex": "51a3ad301c5292dcc208abbe14bae00feebc636408a4b2c54d5edd07ffc492a2" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[0]", - "eip2537_hex": "9324a75f7803c2fb7be84f8f8f92397fc38f438472414438972d85137fa1e884f7fde28c1f4b1d3555b721915e60a4d7" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[1]", - "eip2537_hex": "afabdc807327c5de8fc13495028900d83ac62f2a9533b780582fdd18bd68e5e20287c861c82923e6576330099dec584b" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[2]", - "eip2537_hex": "ab042a4727ee1f333d73706db25bd1f229cb608765128af1b9377b3e2b4795e85503deb7f086fdb781a3c4d190205a40" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[3]", - "eip2537_hex": "986d652bf616055fcb845f79e496bb94a6dcdc7e332a53d2c1d2370a637b0d2a4f2636551e681cf1eb70c561defa62c7" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[4]", - "eip2537_hex": "a24481617df262dbd06b5fddf658f9c32e49c6403bff43252ea10fb35c37d987af93346ecee6db2b794f8ea6b2b60286" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[5]", - "eip2537_hex": "955ddbc47fea8bf6280024d6b455ebb60462d79a19f49187443bcdcec9b16a84f41f41367805521eb89f876c13cc2880" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[6]", - "eip2537_hex": "872ba4345eb8cc6bc2fca678eae0455ed17ceca164901368a453b3826fde369e8cf63db2a87b14fc54073a369ccabfe2" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[7]", - "eip2537_hex": "8e14088f20db449b8f49695c3c84d8d873faedfdde1ebbe87c39c81765d149380c313b4242278b752177fd7b661d0554" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[8]", - "eip2537_hex": "b5e8f709e287c8a92bcfae306d20a15034f8f04e0cd8dee49059feaec3e99e002d4268f36f613598b68b55cc3b6ffaf6" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[9]", - "eip2537_hex": "93c8f3366eb85800f928116e3a177bb36a5baa5b698e70afc838e2e9eac2b35a5a0895da21f21f86b4dd5833447118ac" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[10]", - "eip2537_hex": "a23d93bcefea6c36837e348911ea299e8f478c5097f803c74c9aa0752a09c812749102e3a3321193452d710a95ed97fa" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[11]", - "eip2537_hex": "8c509b6d23069f785cdcac3319040d8b5dd155bac5e4a3b815891e6a571dedf99a2d46b4670dfdaa7c89cf43fc4f7df9" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[12]", - "eip2537_hex": "b642bfcd07db3fb367d968a923c87f6c0db808f812cd9014a01f36d8b1a33b20de6409ebd7db009104cdf07ce9ddd872" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[13]", - "eip2537_hex": "8980d60b9cfe2b6d5327ced4400e8ac611d292991c0abd3a2c7e692ff63b866ebf4c1a54f15c38603e60ec9247578c24" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "advice[14]", - "eip2537_hex": "b946984d70cd450a0f9def10b8f8694e58f74b5cfc521963170307df92baa7e8237fe25d9fe81c0421d909ad0f5a6b76" - } - }, - { - "kind": "Challenge", - "data": { - "name": "theta", - "fe_be_hex": "3ff9db99be64b81421248a42da80d4cd7ba4aa6bb94e8ca2dc7c04b5feaabccf" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_mult", - "eip2537_hex": "883447beb33d2cd632274e5745542ba9f827e3de62e2218a3139751cb8997abcbd90a1ca2dd9be5f6c861b06c2c9ebbc" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_mult", - "eip2537_hex": "a06266251c32ea2ac0f43fb5dcee056bb7e8002fd9ea8480c384085f1e788e542965d48ab07e31d0140987c4b566eca4" - } - }, - { - "kind": "Challenge", - "data": { - "name": "beta", - "fe_be_hex": "5e1004307780e019454167777748c63f1ceb0bfd8137c218f46a29d9fcd8997b" - } - }, - { - "kind": "Challenge", - "data": { - "name": "gamma", - "fe_be_hex": "0058d4bedb482ef6ec68cc4102d79d26a3e68c67a660554306766a08d0b4bae4" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "b992ef1d42c2f3db9b82a33465d87623c31772480224aceaa7bf46ab89cca003b1973ef7cb161a373cb00d968286aafd" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "b124921f1914ac76be4886f24155f31c4ec67e4bfd34b400eb9d1a282310d7c4e7577f80297f48c5e46e17247c9cc03c" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "9215d3b97960403604292c0e49b5560bda50f7de776039dd4792ddf1afcf4485e4036d4c6f3d84c6a730805f433432c8" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "89d2652251e8c6df970de2fc4b5090d192110de217fb3da3b11db3886c472d7b9d4f9b4eac8f599e90071394dc7efe0e" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "86302bed0abdb7528321e4170aaef2962c67c8926908dc990923971736f90f2173ea372b7110afe8d9e393cb73da0677" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "perm_product", - "eip2537_hex": "b36a8b2ee086077c3673c953aa45c68f947735789e2c386fe2c7bc768251bff3eea2ac6681d1ba9dbf24a774ffb41662" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_helper", - "eip2537_hex": "88451ae997148f4b0b042066e35fd50c3a39a24507945b0ea61d3a5557d39e45f01740cc16d469f688730cda358e55fa" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_acc", - "eip2537_hex": "b14d249dd6b8c44b4007503b116d26e684be9ff9d1e76c66b9a0182f4d4a5998bb0bbea3c807f2294543b0651afaaede" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_helper", - "eip2537_hex": "a2d3ee8eea5e7e755b69bb79d8fc18ca688ff7487afb894c3077ab6c68cb8d7151e96009cc5bea912ad2fcca23c63f26" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "lookup_acc", - "eip2537_hex": "84189193c82ea16ce40c6c3a7670c782d9ef22343531180d8f15aef0e3ac7714c6b4d2c3eb58485630260e95a094e50d" - } - }, - { - "kind": "Challenge", - "data": { - "name": "trash_challenge", - "fe_be_hex": "0de9348372df1d1e7432fcb24ef5ecbf1358ae5dbe88097c7df40bfcb8d7889d" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "trashcan", - "eip2537_hex": "a846009a66f994210618221e9d4c3b7dd2830290c5a316a1befb1cb3fa2fc5ef84b679e5e5766914673284cbf6a1f3f8" - } - }, - { - "kind": "Challenge", - "data": { - "name": "y", - "fe_be_hex": "717b58f00b4d3d2793757513329c2c1d50deb5b93956dfd2048548a4d7837d9f" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "quotient_limb", - "eip2537_hex": "b4a018c05d3d9fa4b36bf3f2ae2feca08a658412829a26fe41aec4134158fba7739358e125cea1a5914f72cf9b525e72" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "quotient_limb", - "eip2537_hex": "815a896114a233d654689dafd891428d126e7e620e30dee5a5f1c0d9ceabc0581fa23b37dbd0b7775aeabd9fccb1d3b1" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "quotient_limb", - "eip2537_hex": "b6b9678337e622f2d663b2293748d82b7102ac83213590b5813c5099481fa123f81cf582d5f7b4e81dd3ce024d79097d" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "quotient_limb", - "eip2537_hex": "a2c5f0060ffd5c4f8741b884ee6834a04366739fecb2bbef7f27b8e2d08065b4e979ede81b398a1ac6cad46664035ead" - } - }, - { - "kind": "Challenge", - "data": { - "name": "x", - "fe_be_hex": "25a95a70b527cd00441e66fc7716f297fce60d998bcc0158e816488a393e408c" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "committed_instance_eval", - "fe_be_hex": "0000000000000000000000000000000000000000000000000000000000000000" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[0]", - "fe_be_hex": "2aff2f04fce943e11f15b05b610d1006f7276184a3eb52764a9b162ef19b774b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[1]", - "fe_be_hex": "613ce970681657d1997019c85dd9e468b607ebeb81c81bf5b3e373afcf25e417" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[2]", - "fe_be_hex": "5ad602afd794d4ba4fa04d59b442983c446b7a1924f36a1c869b6fc2718f1b33" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[3]", - "fe_be_hex": "1b19d3101f528b398c5636f9af64ceb203941f5a69906344f6018dcdd93ec9f8" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[4]", - "fe_be_hex": "63f62d35bef1451fe363e8ae48f478aa78d3775c1df49470ed9b08acc8a582f1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[5]", - "fe_be_hex": "6120b59c0ef4b1422d808ba3bf08c4319da59fe04f3ed69e30cca0e30438fc02" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[6]", - "fe_be_hex": "04924d8799758ac570dbce5be1bccde9de9b44f64ba93506721bc8fd9b0bbaed" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[7]", - "fe_be_hex": "2cb4f7a76de56ef73523af97075a0498d7789853d23aad7fc12c7b7f7092a018" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[8]", - "fe_be_hex": "62b0bdacb806ca168215731c666c8d1b3d35241023674f4e8866119719dbfc3f" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[9]", - "fe_be_hex": "5d9ea3fb49590e6dbc8a42f662741fec9a7322411f948f8d3f54bd70191a6f3b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[10]", - "fe_be_hex": "28c0c553847c9c7c586fa0c979a308b2d4c0c87e3c7061d200f7aef29776f159" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[11]", - "fe_be_hex": "48a364fdc95646a925fe21aaf84ed1fc70e97483af6ce14452c251c4fd20ab8c" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[12]", - "fe_be_hex": "4e4e184924e843a78524088430f840c2734476144dd5adf53bb35071bb7b2d0e" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[13]", - "fe_be_hex": "1061413a5fd1787a3f0d54dad842be30a6097a89c52db89360ecd2bbdc98dca9" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[14]", - "fe_be_hex": "64da1115a5c5107dc2aa9304d6dc5b3d83195eb097c28fc05cd57a218fbef7c9" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[15]", - "fe_be_hex": "33ffd290974cedb43eb4e5e4cf9ae0a8c6b1c45b45654b5e960ab34df74efb2a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[16]", - "fe_be_hex": "5cfeb68e3f0b568eadbc559b3466c6d236161a3c8a040c0b4514b8145ce79238" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[17]", - "fe_be_hex": "18e676097fd27a394eecbfc4fc9e77b93bcec22fa31c8cb5df47148a24d61f5a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[18]", - "fe_be_hex": "188899add7daab3c574a1ea69dc2462b98f4784965414b67fe968533a5046841" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[19]", - "fe_be_hex": "678d9102966f8980e4ab679b0e8d89062a8a4b4287350de27b073897526af3d3" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[20]", - "fe_be_hex": "2c69fff1c50460ad0cb18d96216a250fdca786373030ea394cf3e08493085c3b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[21]", - "fe_be_hex": "69e0c4daf6c37e6c659ee977e0f911703782d74a535f30c757b45b83fc865da6" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[22]", - "fe_be_hex": "728344ed27b3ebaa17612c3ffb57a14efb120656258b47fa20c25ee225ef2171" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[23]", - "fe_be_hex": "4806c4286bf892869328dc209bc46314b2759765bdcbcb99744fa0f348545a75" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[24]", - "fe_be_hex": "0af784a7a802b57c08ffaea2b29653005cac85e3332f5cee2b754ab02b5e0bb3" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[25]", - "fe_be_hex": "15d0be507d1046854082c0ae8b1d4ff812a5b0f2899cccfb3d7877f355d5b83b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[26]", - "fe_be_hex": "27f5830c86931649356219918d1fe8ed97eb4600aa3e46b4b173778d7671d50c" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[27]", - "fe_be_hex": "6e3f6903d3dc1e0e195f673938a9edfdd6dd456dcdcd560ad4d9dbfaf85f39cc" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[28]", - "fe_be_hex": "22ca4cfa850bf7487c8d7eb1cf2ec2c86a633768d9b9e2f16919b0830d3fb844" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[29]", - "fe_be_hex": "5a9e3590a4f9be93687d68104c9b6cb2cc9ed410fc07d6162dbc9f64decd7ce1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[30]", - "fe_be_hex": "4b7d002e918dddcd4be4f0623d8f9c0a9533b24af17e5dc634df24e500729dba" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[31]", - "fe_be_hex": "5d3073c3a96253693c8df7ce25e52b38b8c78afd7950a60e21cda35c3e48b78b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[32]", - "fe_be_hex": "2f1400afb1e213b69d45580bf9a0054f90e54c4dfaf340510358368d7eb037fc" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[33]", - "fe_be_hex": "54f6ff91933cf536ecfe439e01b2103b02aff564ad64cc03322803101fba3812" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[34]", - "fe_be_hex": "6bc15393109fc72220322b2e100a293d9b4d7f43828783f18f226701900cb91c" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[35]", - "fe_be_hex": "1071a9bbef3816d70ebf4b92b9755e176e2a8558a3bbb3d16ebe2570ffb34f1b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[36]", - "fe_be_hex": "700c1d9c6e073b01b5c81986fb4df82a1ec87043972046372890ca3c7b5b0928" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[37]", - "fe_be_hex": "64a7cee5b982c3b7eecc70e2a7a96e53be8effb11d1040c9a73a9ebf334b1194" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[38]", - "fe_be_hex": "0175ebff5c51ed1bc0b0ac14b7193dafdf64b1c2cec0c23de2e781fa8394362e" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "advice_eval[39]", - "fe_be_hex": "6955a4c86380c065bd0c80036c3ab85fbdb52dcced432ecd68c48d9a1b274509" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[0]", - "fe_be_hex": "094662684cbadaf67132f94dfe2511cc8aee0f2519f2540b17314664295d2d49" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[1]", - "fe_be_hex": "6249297a890a17fbc31920e6f5b33e284b17b738d945b93af9ac63581f39e926" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[2]", - "fe_be_hex": "533aa4f8f4c2d59ee7d3ef836d846c08608c2c2a0f9322f6f6b269284401b569" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[3]", - "fe_be_hex": "0a7069537b5283f647e1a5fb4510e338d37915aadcab9cee9ac42474f027159e" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[4]", - "fe_be_hex": "036721bd95b1adffb069c6f37846d0949a2541fddded7dbef574c926ea067125" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[5]", - "fe_be_hex": "151dd83de8726f4907cad27fe7c01b6a38b780c9e36a92f2b14b334c6d43d323" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[6]", - "fe_be_hex": "0d8f561572fb44d01ce808d9b57f3dea1151b1c70af6170efd615597953aab90" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[7]", - "fe_be_hex": "388498775fc06d0cede26ab654db23b72fd6d2c693ee2fcebb0c3c8943544618" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[8]", - "fe_be_hex": "3f4cd7d8912aa56d301ccaabfff5b239f7d0b375dc7e59e70ac83d1df74cea7f" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[9]", - "fe_be_hex": "0d28bbe4bb3217c71d9f55cf4e629b6fcbb9cbaf3db566064f6c14d650c3518b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[10]", - "fe_be_hex": "6ee5a51f25ab89cecb00e84084a45fc4f644603406662d4ee5e8f8c93ab72c8e" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[11]", - "fe_be_hex": "6aabae683a739b717d4284d4ce090ca897bf16439d18a537381223febcacf0d1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[12]", - "fe_be_hex": "734189390c0049b73dd5622affb5e10138229f67c4fa0f70f3acc7ac5492da03" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[13]", - "fe_be_hex": "1be1180e1778d1802e467100378415b572888ae535042e01cb3539dd1059352f" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[14]", - "fe_be_hex": "5a8998c3bdc57c8c6bb4b209ce16f2e401655f094e065e08a1b42d14091d6c1b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[15]", - "fe_be_hex": "497165498e6280b15e3e3b2b3bb1be8fafb4880013a2860237039c2872fb5a1d" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "fixed_eval[16]", - "fe_be_hex": "3969731b9c485b627553a8d5c91fee63f06759560d441480d36ab2144c382294" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[0]", - "fe_be_hex": "17e99dd0f4b34d2795f0c82eed3b8081bb499b675d2ef634a8b2b399adf374c3" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[1]", - "fe_be_hex": "434e96f8f1801a73f63d1faf599d28f3c7c9eaa3112979564c2a59a955dd3cd2" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[2]", - "fe_be_hex": "6b5e6841b8101eaa53ccb8eefb7fa79e95275028679301c335ff66f216f41b93" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[3]", - "fe_be_hex": "43752a03cf17d3d0d64777af9a39d283e7859eaac4a2da400baf3af7359a127c" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[4]", - "fe_be_hex": "4ee605378c3ccaf4e48e82fa9f5c6ef2c9bd3a44f482ef00cb170f7901cc2b8b" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[5]", - "fe_be_hex": "0fc1c5e02cfbf5d9115b7137074f6f0f6f294d7777c54d75f5b7c6cca84c03f1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[6]", - "fe_be_hex": "5492e7d49f48fdb68ab634eaf30059712aa662ac23bc8d7bb269afee222f3d93" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[7]", - "fe_be_hex": "09ede7d5f304404ce3602f10ac6c48423349268c98a5e7512c42a85e54a4a8b5" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[8]", - "fe_be_hex": "25bae27aa445934670e7660234eb483b0855de75f8e0e5c25a8859d5b5f1697a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[9]", - "fe_be_hex": "10a09e888fe4fa67cc79bcce1fb2965b6e166238799d9772d47a07b947236493" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[10]", - "fe_be_hex": "22b7aea68284a218fc8a1c85c16f2674c1f9b76494e29be3d5d1d25879b2bfe4" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[11]", - "fe_be_hex": "421d73a8c0aaf6b421264b6ac308d0ba2dd267d3f3134b006b8e440d08e2a0cd" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[12]", - "fe_be_hex": "2fcb6e506ea4f6d5a73ff93256c62ef6c08ce5cb4dfa0834f80fcebabbcd98c0" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[13]", - "fe_be_hex": "2a8865c0d658cdc019c2bad26c10b388ab8138c474013d2618a4a7a9fd78a151" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[14]", - "fe_be_hex": "256e955fd4aca98fc22078c6b2ddfa4535dde657f14193bc91a10e81c6ac8637" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[15]", - "fe_be_hex": "3b36739c2dab05e8731283b6a33ae8a8f899a4dce36067d38876c5603e6d4dfa" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[16]", - "fe_be_hex": "71de8b575f2b8d2f97d31a3877fcaec1603135e3648dc8fa91ed9a159dc72026" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_common[17]", - "fe_be_hex": "2693d914e4ec4b414043dd7cd62baad35ef74a798d63641cbb89b9adaa44b2e5" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "6f5d99221a6baa6fdd4a86dccdaecda185b900e389abb4aebe6a78dae9986dd6" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "09e25e6e94f2d464bc6dfbdb00dec371465c9e63b351f030faa16f2fc9089729" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_last", - "fe_be_hex": "6d7e57bcc8a142e541bd48302d7d5f09ced322a30f7f46f5ebd3f068934ff3c5" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "09e6e9d001c1722bef8caaa1defa9016313d4ad09c6ab2a025c873fdf217a88a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "3c4fadc803f4d688529db1ab405f4b7c87d62b8312d06b82580e09f83509eaf8" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_last", - "fe_be_hex": "35a66b9743844178d52f221e577f21da84afa99357bd796ab5ea58a44000abaa" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "0a1d4b022e51379ad4fb5e3dd2ef2d85782787f64cdc57b69e4eefdea908c5be" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "04411e7377b1c6c426bfc47ff792dc95b65276cb44d3e3742944dadd2356ecd7" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_last", - "fe_be_hex": "3a8947962b0f02550c0eb3ca374c7dad62e6a93f6beac0b717f0ecd53ddc08db" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "530b29213aa759519d799bcfced039200fdc999b52b0a3a805fe5f17a87ec4f1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "610e49e355e2d7b519059f2a05cee82ba7af81ccc8521c00c05a299b43afad72" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_last", - "fe_be_hex": "4c4ae9b7ff00533557189e0d7e3d92968a22b735511793c9b1cfd24ddb481426" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "360e366879475eaf65c181ee1ec4f7cb7505e72c2cbbf909ba9501657943c7e6" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "6793d5eb4cab328ec774e8790d4fa5f7e2eb2fcb326aaad5da8d7902bfbb0cbb" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_last", - "fe_be_hex": "61bef43e11955c61a073955b26c7a5100c70f40cbc557d72efd31d37de08eb99" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_cur", - "fe_be_hex": "4f8352e3683141c181c8b3f2c36fb3c87d307f2ce83460476b2abc3e00e367ba" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "perm_next", - "fe_be_hex": "6a3393d0a58d7eff8d70efe2ca1651abea2810066637f38d47d3fe505e6a66b1" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_m_eval", - "fe_be_hex": "5db5037d269fe3b3163aee34efa1700b945b64eea07a169e05c1beec055879b3" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_helper_eval", - "fe_be_hex": "739afd93c5d8099da5c1a1a38c3b931c0b9d78ad5ac1b0cc8a5a02fd0b9b8290" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_acc_eval", - "fe_be_hex": "3c654f49671abd7b7d5e4a2dc444a8b59ba8be9a5422bfc776344bcda00f2575" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_acc_next_eval", - "fe_be_hex": "5c73faba39c84ccb0f0a9be5f7afad39553d992f22d65e43370137f7f1748ea8" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_m_eval", - "fe_be_hex": "37b288ce777b7ad710ca063faeca1c7e830994011132bee36592dab105910e30" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_helper_eval", - "fe_be_hex": "0b10469cb107cf10a1323b9a3488bcb5070d70e57cec1012a0d832cea8f50d5a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_acc_eval", - "fe_be_hex": "1f5f07b0f2b3157e0b593ffe0cd4c03602ebba3bce94a97d1416c12c0c04cb13" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "lookup_acc_next_eval", - "fe_be_hex": "19aea3331455c4d6a7d7e0396441b078d62d74d9b13021572a4bd8f9ac98353d" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "trash_eval", - "fe_be_hex": "2c5b5194dadc6558b5486ac297257d2f731ab83bf4a6de7f1adb44611ad985be" - } - }, - { - "kind": "Challenge", - "data": { - "name": "x1", - "fe_be_hex": "13f8c24186a7be1a397c7b2859ff227b2d0920d32d545885da542bbed0ae254f" - } - }, - { - "kind": "Challenge", - "data": { - "name": "x2", - "fe_be_hex": "29175d62cd5f3b273588fab38705600a7614b67768c291d31130891f1199a011" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "f_com", - "eip2537_hex": "a6812bc568c0e0aa3aafc20385685e504c2f36778180e78ca44ab76a64b6be3b77c87f80da055bbd519aba9d5122e459" - } - }, - { - "kind": "Challenge", - "data": { - "name": "x3", - "fe_be_hex": "58a0feaa98295060f65934374141b1b8720018a665345b485d2fc89de8704cde" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "q_eval", - "fe_be_hex": "15874d911e497a01d83006b38e4b7fd697d1cbc335f9119f085739db694a5a85" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "q_eval", - "fe_be_hex": "34d56361507e3fe5e2faf8f8289d4fb0b5a83072b4fe6b898b1fab754381561d" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "q_eval", - "fe_be_hex": "4928e1876a849e1c27ee7536482186bf9359d26c1d347cde0fcf83aa529f147a" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "q_eval", - "fe_be_hex": "48ac5842c24471f2d12739e9db0f841e972ee94549e5a16531e6b0972273c8a2" - } - }, - { - "kind": "ReadScalar", - "data": { - "tag": "q_eval", - "fe_be_hex": "4ec3466b72d2b0ea1ab18978b23d2da7d01b8b040443598bbe13c6b04d404983" - } - }, - { - "kind": "Challenge", - "data": { - "name": "x4", - "fe_be_hex": "0406be097ef26b66a1727792c8300aca3e81715664c38d1284b4d40ea47edc67" - } - }, - { - "kind": "ReadPoint", - "data": { - "tag": "pi", - "eip2537_hex": "a9f4920b7a8364d6c85cfc71346be70f46dd9ecd7fd527f2593c83cb55359ae395f091c8c26289f663cbcc7bc945b923" - } - } - ] -} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json b/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json deleted file mode 100644 index 287b943bc..000000000 --- a/proofs/solidity-verifier/fixtures/ivc/solidity_trace.json +++ /dev/null @@ -1 +0,0 @@ -[{"kind":"ReadPoint","tag":"advice","eip2537_hex":"b5a3ed19b468966d517a4246fc2d3dffeff693cfae260ebca3c20570b03d90bb67818f1f29fc5eed276dd931d0a8f744"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"81725d4f826bd4e1b2d44973a7cafaa3ef68635e81b756986e1e7e9c5aa88dee5b132e5675c53cabc1a6650d4f7724b5"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8405678eca7b3899fe8a5151c2f120af233db43316370079b635b0a1b586a11f88640a51e8fbc3b828a9c5761aa247e8"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a94e856a5ce18ebd3ac321a8e927365411113a2c94c5e318351aea31e2927b998ed7aa69f7ab94eacfec259a90944f4a"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"9927ed8349c0e9a120abd70a94b2be16d801db78a48ebf1b8e832c12bad6b1439d2449a232fb7834c9ee802a9c17b99a"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"87f25afb4c569da357338c5a8d6f6f8f9a70d3ee4c4c76112ce557120a6c6675e7a050cf6e31a3bf9810ceb691fb5939"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a901dbf17bda36068a654324f80bbb5cb662cb3b6b6c8de00c092be6d339f7089b92885504c313ce6942de14bb465b5c"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"895a3b7f738bb1df304e5dbe3d3f053598e3eb2e421980ad161aabc942edefac85393611ec42fd40d0374bbf924f405e"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"9662fc7620753324b71ae06218c1b5e8089bb31839463665723d8fbe7a2ad245e5133c774bdf716041751bd486235a32"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8024ab67e2e9b10695f9d9de724f8150fbe2ae651c15270b46ea58edab90cd9458f2384ba9d3b5fab6645dcda696a95b"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"940bca31980207633c105e4723789eb5f4127c41f6325b855daf2cf3180eeab52b72f734549b72b57f0a27f787a5ca07"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8cfa3c33b295c80d24437052ff753e93461752b8ab0170a48208d445658e53f6fb76bba84de7344233644bb68650a7d8"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"a7390d9b294df6d8c18c35c3ea28883c1c4cd48fbc12e8d82928d1e2dc340cd64defac67dca106a37967fab7fa8d3cb0"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"acc0d4d4feab2c840d1eecde2bc9f96bc6d2cda78ff298ec6716da118151cf598938b6c9346d8037a02b2e241dd1a3f6"},{"kind":"ReadPoint","tag":"advice","eip2537_hex":"8af8bc94ee0178b5f139926856976d0253940d5cb5391b39b9b4cf2c06f3ba7289bd798230be9a834976ae152441886b"},{"kind":"Challenge","tag":"theta","fe_be_hex":"27e8037bbfbfd47ffd3c2ce862e3acf6b81a0a6619471248726ed6576227807f"},{"kind":"ReadPoint","tag":"lookup_mult","eip2537_hex":"8309bd912ca8cf399279d7dbc8f453e873317ebcbc5e510acb13e62904711a628ff30b61dbbf04f66ef56f464056c3f4"},{"kind":"ReadPoint","tag":"lookup_mult","eip2537_hex":"873b6c597af9d183834ecbb4ba28a741e5a76ca027e14dccb5018f7cb527c07bceae9bfbab353f064720b0df70515ff5"},{"kind":"Challenge","tag":"beta","fe_be_hex":"12bc4f0d2f0d0c27bdaf7b29adc5b364ef4eb3550618960d1e6e7699f4b71760"},{"kind":"Challenge","tag":"gamma","fe_be_hex":"32539d02a7dc77ccee137a40521fe9a0adca56d16214e383a1458d03ea2ddd15"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"868f44698ed2b1174d655ade22a1bcabde75534c1af29d4c646df25add46d276e58066d8ffc6d7a7221c93669959d1fe"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"a734a580452233277b40065adb5f3145c701f870e3f8a07cf401ac04ca5787e44b551f11f521813b76db5ab41ce56b83"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"84da1d462aee0e1f8e2b08ceea88ac0b7f60842a6ecd97428bafa652e50b722f646dcc6d5a3b41ad5a5ead0178a9775e"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"88677162fef7cc540a46813c82a43da84b476a8726e6e376faaabe15d30e379aa0a902b5c962cfcad26f0b6b7046df38"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"94ade48a9857fdd229d84e556f6e483c512cdd54dd550503686b8317d6d99fee43f79390e91b5ebd303cf0c6bf32f06a"},{"kind":"ReadPoint","tag":"perm_product","eip2537_hex":"adda51a9ad06282aeaf8d1f2d5e7e7ffdbc3e89f0748db870157cf2ba3de8053d731c204001c8063a3b4e18e0981adb0"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"906be3c46e0499a65febbc526c0af1d4c7f0d630af1f76ff05f3da874adfa98f83d8a9d048a94b06bccd71f1d8370e22"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"a963d0a8790cec1acad417d575f03d18963b97d0b8699fde81074471ab5ff84b6b2435e4de358646f2f8cbedd20781b9"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"8d562aa58c17eb6a5bead6f10b62ca93dde62817d7b1c8d37d60ebc7699f07bdd4c1df103a6308381b6ce1ef62758956"},{"kind":"ReadPoint","tag":"lookup","eip2537_hex":"82aee8ae26dfa6e8cd888e145c7a221fc4b4179bfc7aadccfd173129b1ae29c28f8f8cfd0b68e00142592aaae2697662"},{"kind":"Challenge","tag":"trash_challenge","fe_be_hex":"1f4aa81eea4602bcd9a4d8c413c83d27a205f49d074b3e58d25d8202feae2b60"},{"kind":"ReadPoint","tag":"trashcan","eip2537_hex":"ac938f8075047aec1fa27b4a958db254a15380ba93ec93b19f5cf8a8be1d4078e74b1b1a0a70d907f5ce640816ff1d15"},{"kind":"Challenge","tag":"y","fe_be_hex":"4b77a617a03318cfb24402ba260a8d4b6bb38e03827db22b36d6196840061025"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"ad70a4fd9070cbb6a2dd9a302486e2158fcb67de21eb59cee8603396e869f34c7dc3eb5bdfe3cb6a99629ee6524662d0"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"a71a43c4506df681ca8112d94d5f57cdeee6c1be7c875f0cfac40e8291bb52dbb147c0655b7c15aef823e5778f764ae7"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"b1fe3bf128c226686b753e4bae0a9f7633e3ff0d3905e0dd90e926578fd1d9bfc270e4faec8ea370827f85283fb5231f"},{"kind":"ReadPoint","tag":"quotient_limb","eip2537_hex":"af288e6aba3df8ac49d2320b0d2d8e1bbf9665922e962bf2047178ba018bd55b6c6494c59f6e55db80cef009dd2ecae3"},{"kind":"Challenge","tag":"x","fe_be_hex":"480c85d8d1c587dac3891e48a29ade9ecf1674679a0b808e8d2cb5bb3491f2ea"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"09ef5b8920d5d73deec7f3a353ac34262031dbd8f12384123a56b0444384a71b"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5492a2956c450bac44d8590d6d8802e03f4951161e6b8b638a70f02fa6f8679e"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"201b58816342ae2f3b644148e9df8dba8e4df21d31e77e57d14c192830856d0a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3e8973b8bec854146133131923f26a622f86d10f3729e5a033972672d2ba18aa"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"08476b87f42a92ea6faee7430cf19849167bee80d0903e2b94ff68d20d067e16"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4a5f88276bae0a6dc5737b3acc98ef978311a886979287c51213f09e50700dbc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4ddbb2b1a19d17740886685096269f5cfeedecbb4f842b69e18da0667676652c"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"64b29d580131ef7df38067302827572041f0284568335e3a06a9a73b736e2acc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2cff007180182c6e10eb772a9093d7b2f42a77e82888711d3e74bd370b605d03"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0eb888285a964a03a607bcfdd1ba5ad71072b5e96901bace474d434cbeb1c35a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d1f55b5e070b508ff54a51106ad265408ba62256d10f0aeaed5dc756e33bf51"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0843b40b6701ff67871c911b0bb6768586ba9c034200dea8eff3dc70bc130b66"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3f9f6a7efb75d7c4571bc4154df2fa8c95e1df48ac1a1e3b32f0f3920d14bb16"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"19242199f60bd966b08275c90466ac422e5945d0ec08b26fb6178369d6b32abb"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5be8a576c4e42c8e6698aa6424296843af9218bc4d67105d6c77e792fb936cf7"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"54c32b308570688ccb4a3d9877f1667393f9c775a190d15bd789be77a7872637"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"65dc496a78bb27bea7e832fb2fcc519ca0a1a0e9f1a3da01fb7842400f84596e"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2fd8a069b0528005bad464a072e383372b46dba2bb12557d1c0620f48520d260"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2c925fce6a934219fbbab1c5a2423edaa05d161143fc7731319d38ebab1076c0"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"4232af1b388ff9803753312c54e93a055085b754c3f051507f855cef7ab8415a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"49b4754ff126553764ae10edbd8caa67b7ff08b4acbb4cf9403588bc12e7b9c4"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"62a4bdee4055ebda8b75c8c458c6d8ceedabc9c82abb574ca64d0d0d54265b49"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"53d0c5cdfd35487168e993afcc0857ac972e732b06d3d21e33d60b8153c73d9a"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"2c3fba7975d6454c64a7806254b583692fb94c78221cd2d50256d03e47a71146"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"596f33fe28d30a2cd4c578e366568a4fd773643713d11c317a74d1c324e7fd23"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"049d1ebe2161d109d340ae7152df2f4831ad22fec7548578283d91d7607e6474"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"6627866c47eba59bd8337e7ef01a0067277a257293e77d9cdb1f317de54c7074"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"34f29783304a2e64bdced10de519a3550478203d00d69574dad8df6020cdcd30"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"48362875e1a6d36ba286b3a5a8a538d437762d7364b33c3c3b742b2aa58913e3"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"61935b799327cf51fc7b3e9d3662669d75026e5bd83f8bb50da9c277a5903dfc"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"44c545a9fb7660541a696d877c75b4dc275948679124d98196f050c72b3bb4db"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"27a1235745ef505fe3cc0e473f494379096496a9f239282f5380ea91e9d4c0f2"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"3197b3329a543c53400fa92f0ef912fe3f12265a7a9ebfd1fad1c96c5b4b7573"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"0c6b7da40f51cefedc5e562d996225571e96da7ee9afce17a551f343f6893a21"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d219cb7e0e9fe1c009f965566fbfa71aa4567bcd937963627640f6eaa6cc77c"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"1d871e9691901bfd6d4089a6cf77afc86504b1521e3ecca9172bce5984df3c07"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"5ac394b97b804f3b8557fc3b67d2ea12919e27a9f1c33aa3470db54e493f30c1"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"46d90b6372812f62be94add1c0914df911e1c520e0d7776556696a3be3f2a2ee"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"461997adfb8e95e75c697a3975ea365a6667e975e90fc5520d86a2e19a8d5e79"},{"kind":"ReadScalar","tag":"advice_eval","fe_be_hex":"019b99c3d5c202bbabc76aa2a2a86c9e445988e698197b96cfb09dfd7311511b"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"14ad4103f2e8bc5ddca276f21b54e3ee4054a6e21b9d567e71a819478626cf80"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"5b6dfb1926f4165466b1c34793247660c1ecd8244a2eab3340aee10747bdf466"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"2414f225327ed2c7ddf7b77b5d10f05d6aa9634913a35c2e5138611de4a6cfc9"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"6ab6d2f1fc1ced14f72c7a8cdcf48abba1e048161d8a720c7a290853956f1e2d"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"6dbc0726ddbd4e1cbaf60f38999498eb2e8eb9d5010f2300594d3703be266658"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"37726ba651383f231e61b5cf282561273a93f0344d01d68d4dc209e724395d1c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3e68bc805df454fca334e1720f57a3363811f1c17808da70f205bca79f0d599c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3b40f386c432f27daf4e49a626e983cc4a8bb5501f0fef370a463cfed9d63aff"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"5e34b61c1b3db7f63cfd45d3e261d300e858536fd81612be8f50451447b519a2"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"2f16371f1d896cd93e2201b39fc4ba25d2f6d09d8b82ac12e46e930ed3ac21fa"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"0ea3f215bfe32c3166b8abbf36962f3ece721ff0bd303c5c59bf566521bf1791"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"004f497fb64bacf49f0e7273047e805a2c1af696595c259f8a73a49bdb41799c"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"3e06211806b4e7b4674d4a2dd409484190ecb8158387b9d9be9940e35fdf9b1e"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"1db7203289fa5f6db1e2b150057157b1da3fc8b87a54b62f42f3521edaeade54"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"01bbc3918a1edda73c72ef9c0ccc91b584fd1915e469bb9355897c37515e0075"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"1134aa786c389ecc8afb97cc1f80c6d17fe4b0a282371404e99daf7e22748a0a"},{"kind":"ReadScalar","tag":"fixed_eval","fe_be_hex":"44154629726a79952b69531e24d1c476a100c387864071ce9873ca0bd4fb8259"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6c9e183607a90e4d710b847ed3082cb90e11430e161e180fb4cf6dff5e60f6fe"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"712d02268b3d16ffd37948ddbf54e8e0476873c1a561ed9a50983d8c3af196a6"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"5839aa24628fdcc4ee441615e0f60859084e903ca95b43bd89aee00ff1accc76"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"37f5ac771c0edd9ef214a34b33a469bbac297bc17e16ba72be1c046fe41fbaae"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"655ecb16378f8d990cf6c442d21bc690ab2ce050f6af93c8d94357f3979e72e0"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"3241d62aa2ee77caa09d00c1048f1b383f5f632cc741a71a382d6912a5f0350e"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"373f161bec38765b8579fae5f9057301be67013bb38928b5776909ea08337edb"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"0dc1d24a4f7bd8331c394efe98e5f452ff3955e737ddae522f9e2552ca79280b"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"516c5fae7051b044cc4798c9f75bbcf06c4b5910f882f1ffe15e82f329165c6c"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"3de4833d15a75e94c5fc30c03ec19c7e238058d2fa242e7caf17d6b7b57ee2a1"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"201cd483ad4dc7d466cb4e9e2ea6785b921cc491bfb23d121a8a4bd1c314af48"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"18f0620cfe7ecbea2b26b6419cf1afa321bef2146fd0e26398d201b2b68cefe7"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"0a2b19de67150f439452406bb4d962da831d80241e625cd714e9bd0a413ffc1a"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"19a2b386a5a2414caf2d9a688b4bb38364810775cc9fde2ed1804d34a1e4905e"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"4729b5d831af837bbb9f838f11dce195a82d1694320c7e9e98808e77b5fe556c"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6307e640d82663d46481590a917b746fb0627a5f80dec829c691d0a442dead1f"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"64ff573b09c5d6897d782bb730572d1f05f73ae3c9ab8b7dcef6a8312df95c23"},{"kind":"ReadScalar","tag":"perm_common_eval","fe_be_hex":"6b8bd275e850818bc69ff00f3b8be5d689eb8cf7be45af436d2317ad048d8a73"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"69f0d9446331009d2881069dc939974374d361c058e4419736d747d753d8e4d5"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"5f14c13215ffc96f43762265ec6be1523aa467caaa8dbbed4fb5250f6f5fd26b"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"08c5f4d6099af4b9b425951caeec5b2034cc116c781005a1855c1b0809acca87"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"09da2149c10bf24f5398bb9ffce65639c62f5e65eb46c9088527dfb3058f0281"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"324f6ef7243ba96253f8669000cdacc095449ab947719ea8de02b57ef087da6b"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"503c69e9a786ddd9cc47b1a7b33c24d9125ee2d3a8a73812ddfeb2e2820c5b81"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"3b45aab1197f9e693e77ffbbf656671c103e7cc7fabd7676cfba7c0dbe1e6910"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"68038ff1509fd4631d2d8ca7a62939990052c92827ae6692656bd3c2e18ae3a5"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"52f75632c182416b0ec8eae43b99ba8f4cb07f305d4a23e7328433ea7cdcccaf"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"5c3379b067d47d07f0baee88a7f96a71809523bc2122e94ce803a77bd790b7d1"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"142f5d5ffedf93ab9733f1d1c1322d7a2a787636328b35c11e0e9336c9776233"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"1dd4d02fab4470903c87e221cbef954679ffd8190fa9df7d19ccfa9730c1d32d"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"0311fe738e056e8db829dbb795c41570a66b8de508afba7753313a8e0f64b1f2"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"5296e8474e56d657c049421b19587912eedfe9a88ac5f3167eb63ddd4bcec4fb"},{"kind":"ReadScalar","tag":"perm_last","fe_be_hex":"04263ab0bcb0df4e05cab8ad7e3033bf7a14d5b0bc2bce18deacff2e36cd1741"},{"kind":"ReadScalar","tag":"perm_cur","fe_be_hex":"24cdd547d581885a9499b9dcfeb3daf3f6b2573249433b68ae4b06e5c6e17145"},{"kind":"ReadScalar","tag":"perm_next","fe_be_hex":"066b9b55e4b6942a9bd6a7d37d970a88f38772d00e3955947d7288267dacbdc3"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"68ee03441d1c0f093ed8fabea2015e3444c1d9ae728a6fe6da42fb84a98a8e73"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"18425279cb0d2ea5a3fe6a9008003baf88d8609655e501bb056d6fac2b3a8154"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"08f9d736bb32f4fc2fd22f40ce8d737e27c02aef5ecadd2117e182ccfec4907d"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"52cf1481bbb3ab0dcb051cff44bc54d8719152970ff508290418e0c36d59ac9c"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"160dae4607f1df78f3391e992a9fe6f757227067b7ec1d0d02ec14c1d9bee8db"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"44cb92fc492e5dc73f69d9cd84c668734a36de040e2b9fd5f158fca36f9c3f10"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"1ca147219543e05d489c2a27ea73f94cff979d018a579df61bfe1fc962913b33"},{"kind":"ReadScalar","tag":"lookup_eval","fe_be_hex":"0a163b1652cc1a06d13c3d35fa936ddf34bbc1ca841c6c39204a133da267c0ac"},{"kind":"ReadScalar","tag":"trash_eval","fe_be_hex":"2236f4d3167396b4d87b1d7defd0e879ae415244035f3d7eaff75943bb4ad383"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4d84f602435f12f8f5b1345a446e1d5af680c023f2e1bec4cd2971d49c4ca92a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"38e4c99b63fb2f92a207528b53ec6384413939f13d1d5eeb6e40d401095cccf9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6a115d18c53d7506b6e98cccbce5760ac0103a70bde718afc76d989e92a49d61"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"11e74c9d92f5333b73311b9d81ddea08e8048e9693c54c3f7f522886cd4fe73f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4b6767e53ed2bfe61aec89506ebb5d11422fa20f171424c75f1a578e5f14499b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"39df664536219ae8c6d13cd65b0542a4f37585fc551cc32da9e8bf19b619a325"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0493e84246159d9a0c5ee0b0d451fbafce46f9bb9a5c2845327628d26f1e7bbb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1cd2ab993c64e766697fe1b0129938a0944298703452af46be29d5522aa88970"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5619c9c33806fb0e1f45ca1279b1c6303b9aa577ffceb8fd1ce85debe63e8b23"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4cc558c2bd77616304b3fb7b7d77a5e9b15abe455dde10e63182f80335a95e5f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"689634165a2730277593e57798b7c066dc5186379d0b15d4f4d7856982daffcc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"355b92c70ec6af51ebf804e565c7d1a1228564101c77f73abe0c3c88aeaba470"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"461f03079bbefd53cecb5f46fb80476a0195d44d635ee14f9b29097c0a631efc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2ad47773f05f81e86ad287bea69ff972ac8ff4af5fb9b08d585309ad53f6daa7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"57261f6222454259b1b70bf4b5ddb3e090b3900b7efa6a8b536d45a4518d9724"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"057ac9a6d8cdf2c93f6014f0f10b9a2002bfd4efd1a98dda624e3b5f17e94834"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"26602d5b7d46eae0be328ee647587ffc128d4bdf58d5347e22aeb4f828c4d248"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"381b2bcb3e25616c0707c9d717197f9fd63a65037515d07211b05f3d6c9f58ae"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6ca1d68e1a6b7792a57bfb3b936070ec9e2b62dfd80edf6bb011736de7a474bf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"42ef248d0a8c8391546451c282110ab5048f334383c82ec31b444d4ceb3096ce"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"55e21e3da2832df2f41d6a6f43a020f3f4bd2ab7bece259d999bc908f0abec2c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1c53991259ba6373b5823de5dfd510952bac4a31af688a42635a506aa3d1799f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0f79a34578494b0e29f69a1df1939463ec999607412f22275623c241bd3c3fee"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04aa7cd595d0166bddec5fe573ed2c525ad1227e5476c7def868f5b264954750"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"18676fbe593dcd108833fc5da5423a9f916fa9a30bd080fbdd93315cd4d47dbc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"131071e42b01b53cc5a69662719d931d7d7b9990e703a53ae84096d048f9ae3c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01675aadf0c0d8681f8d27bc981bd2d2111820e15c7fdf04009c6e0af1f455c5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5e27b5e1f5a3260b0700251b06342a148fff1c9ac5ad4432f0d95c3c658c56fa"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2c97d440a8c86cc5ac8ae887b62f78ba1adeec390673cee6fdd679465c0794d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1824e3e45ab7c6b5353d2d250fdad905e8465daa05c2a47572b791f92cd14ff5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0f80a9f7bc25fe7c2b512bfd60b4823141eef42adf188c14242f8bf78f6c9fbd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6940b7e93ea186db8c9089f0730eabea3b14a6f2bcc4d5f640e79a3df0e9629f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"17fd958f28b995d154f198bc0da303f14ca5f808705a77dfd144ed6a94d9249e"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5f0ae1389418a52788093ba4a333c9fe08f8ad8625d3d0f126ff9593deee4a98"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48ffcedfc3681408df77ecf6e1b6b1010e76eedecb534506435060091ecd2625"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"44046630593df43fc89df0f40ef2825e44d3060718c5236f3766852e595e1d65"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"22201493963df06b078882d59ef1baf520c7f8a38e683270f208fe3f11b5da1c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01bad9a7a03e41cd6bd172f3fc4e62fa729a77cf2e722733d242526d2f5a7ecd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"12cc9895e0990ea45556a45312c508ae9fe8f8573484156ef20db108976a284b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"216b01ea2caf761404161e279de3dc4f62da3a8dd4f469706ce0a1bb3b61448b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1b67dd2a644fc0d81bf61ca12d534a66a0220548880d3daa2dd459dae236b0de"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"565997706ebffb9d7754038a6c62645854f0df8090e0708df29c899e99022b56"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2b6581101163f525d224824c36435408e6fc905e3f317353314613def0a07518"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"68e20f87934b626a2d5f7c209d869401839a4a118b5bb8b178ce3090d20e8487"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"21d1f9e69bd479903c92a4303b730a30323e07b9bd09962714fc3e7287beb409"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"45ba1a715ae585a0893569cf924a6b7489ae3f09e258430d2b339b8ccde77007"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"280ad47f4e418e50f544e959e498e91b26f1c0d6f52338b60acda2c062ebdf30"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6ff36b9a1df788dcb81f4f9a55e7b03fa49fc3b7e6d7c25316cdbd2014f48e81"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04dbf5bc21710d87e2f53f794c449b940c3dbf98d0c3715039be44948efee6e3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4cce456370593b4b2c12338ba432d7d2dc8d2b954fc27ad7839953b388c7fa40"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"245136107ff6c9127cd806b4c4d551c870e803b2d6bcac2a4b8488326f5dde50"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"037537ee588437badb0e3f22d0d7bfc2cc529bd03dd760e06bfaa4814caa14ed"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"06bdcd39f7b0136caabbea18e942888444142bde27356b103aeed6b9b10613a7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4938b50ea746606c644b6de8e69de0a1e1864b137526b661ea92e5f23e862012"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2d42164ca6c6c3fba64ca662762968296b76eaefdbcc2f72ce6c659730f06303"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2cf5ed389a2b092dde54c11fc598b5296f3f9ea6083366b8e08af58d2424797a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5e2e4955bb1026354983477b6391a75eef3ec35abc89ca0ad9b2836bbe052efd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"23b0a43901f778461e3e19850f0a644639f5b098404462b28f9eb11dcf5c943d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"42c426784977ee16629b7b136ffd514988b085edb2b2b5732ff6ddef06212b65"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"510c8c528c8aaefc6e7ee6636a68fe5c0d6676244c76e739f6db4350657ee7db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1f5fa161bd17eb5cd7f51486008198a2288cfc068de4c1d52f7ce9165dbd1a32"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"723089e8c78223ff6bcf157f5d7795048d0e5f342272e157c99fca5221b91c9f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3819ad665aa0f4079328407614b2763c879aa0688462dd1d2a054a692fd625db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"65714decc38fc058c4c9647ba3bda9deaa1a66c396f0a2ff9882db4e4bb6eed5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"11a51b2754bd7e318250260b9e937f8097aa6f5c456ac5ba7b108dec1f4e94b6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2b94f00cf585aac715f80005811db45996cb13a743397362fcf9c66575e11c79"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0a685a3e3bca3ff7b2c6a384a0bf9f35d9a2dcdc359de738765268cb3168cb67"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"44ea929ce6e2d770840cd5e995b12643282cae1749c019df2a74fa328152b2d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"45c2845b5844106df6f399279ac4df7145f86caaabafdc21285923a6a74179b3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4d41d079b8ccb0700381f9cd3445420ce2f7216919d2476d167e16ce020ba1d3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4050fc9352e2344291a6150266d4ee85b3f348bf9e048d5b959cb418244e9d1c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"241730773066d495dc9d78b9ae20bb88625f7db54fe8922d10cf63d3f1faeafe"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"10c4dee4dd83ea316988a170a4b5f98b098131f919bba00b0b7bd458fcd8da12"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"34fe8bc397563d8317fbe40dde895fdbffb7c69d9c11cc24330d41b9b4db242b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1186fce670c53ea565b4cc9bedd44983fa02f3ed77004f78630248955dd8fcbf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2c9d92613f842529943da94911cd4d53d89144aad4f196d65f9282d774ebe3ef"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"43c252a5adfbab1141b203505fe28a11d67c3578e268e74ec5cea27ff466fa8a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0aec0d131103b9c6e998dbc5e145158df9b668512ea007b84c8d2a69589e57c6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"16fdd35187250871cabce08b82f94ad2a6b5e64038cebc13c3a193b2aed03525"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"52eb863553792fd28628cbe7c673c265f0200c836e8bbb3a3ad10e9dd2788f4d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"044b950152a054968cadfa9f38f136dafcde35c8b00c1ef07231244c04ba0a44"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"461715185e0141c19d67c47351d7ad78683e1ef1b5b1d2fb337f3e0ec3f3b249"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3a6860e88b5723601efcdd3c47c13c7cd78f0f1b718369b068c8c8dff92ef071"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6f180c1c3141f5d42fa4cdb774114b9a7032ef944c6a0199443f966f63c2a1e2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"63c9fc3b50a25e5a8ae95997e46f3c582543251ad166556ac863ac987e6bc10f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"30d882588ee8f6959b437bccc3d6ed9d2deeca82d24a1f26e2d7f22f6914082c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04922982ba64fdfb4fd3d8a4fa3c312ae96ed18a2070d900179d47cf710cba27"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5d7d715c6f859bff0b379c04001acc2755621cf4c9b7d83d0aea9ca516340585"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"193e3bd8a15fd20590d839dcded732f17f97d47434bffc3831de95caedd7bf8f"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"152104cd9b83bc618b92993d360b4d04e7478fff837cd4af8da3e64aa9c19f3c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"447f5f3b822ad2157c6cd978078607526759f0f312beb0cf8753ae893f7bc736"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"35a49b4552bc2897670723e74f0e687ee1cab24d67a67711f030e305d8852c51"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1c23439d4cf76c447fb8afd031d562c07f75d0e5e5c91950c0e4528ed7b3110c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5aa38b685836bb01905355f1faaa3eb0bf44091f957fedb8866431b50ca21ddb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2fb774f3e1d4ca762d402be85203ceff34ddeab9e654270864c40042cf693ae6"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4220c142b4b52f30d04f8c217fcc3f59eebbea5780acbcb6189586b586e9f4ae"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1f72922c6c67a07ac1a4ab00aeba2a2d206a979104f8332a97a42a1ba43f1914"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"378647b10cb904a603e7d8f71c01f8ae0bb3b3625d9fa395c4bb52e839cd6efd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"67487522f6a74fa7cef58cbb07683df5491680ef85085fa407036639950ae5d1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"19937dfeefd2158ca0bc9e683b1d325bb5c90053db94f517c6ceb9af4eb40c86"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"58745b577b25297b5690da2a6f756362e2886372b5728c9179f4009b92597fb5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"245ab486accc627b29a57d4bb9cdc50562eb3e8f2e013c55dde35f1a4c4afc85"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"01d265e5bc50c71b71644720d66cae2fcadfcfb9cd57bad131cfd436f16b3ad4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3cae55f63a45e8ff3ecc07611f0de9d5091af977e5a9e37b00912cc8f65450d7"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4562d249ab3ab25956c3de8943594e55aecd3e5c3415883a06ee8194a04ecd77"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3178d11e65909267934d86293bb734ac15e64585f69abfc3e8a96ac96efd5443"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6626afb1b7f2e317e5ee2112695b983b77158e0047d819aaf887ff0c6fd64422"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"353fd5b33bdaa47a4edf336f9ae45d83385458c9e6d038b44090bc38a29b05de"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48996b81338a8b87f989f69eeacecba348d0bcd96e5b9dfc78ec615ff3f572e4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0896da0029fc2c69b7558bb1b17603cf24b45717eb7bb1af86712ba784679e18"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"130fb75572e05392617c3f551305841d859e7e066fc9dfdad0ef4df6ebb8cb5a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"49c9e00faaa7ed77efe0a387f3c5071503e0e530e83fe1356b42aa564f62e9f4"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"60cef6247d3feb13cdd27653bf4fafa8401af878a97e0a8e5dff84f26b147588"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"07e3202eb6b6e74418ee84c8323af991b2667a27b9a3da4b3913b67a3acc3ef3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3935077739ab4d47a0618aef4f8e5d30b10b37f9339175d9eb1b636be268b2fd"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5862cf4d8acb32ed43215038ef1c7e21aef91bb8a36a55f8109900bfb155375b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"73988db21c1f117f899f4c54bdf532c9b9cb10265f18300aec5a91a6fb107812"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1947314d727a59bfd9e04162b2e12cd90e4cd82dfd8de26e9dc97b7e78ece0be"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0ac408ecdb6ad3b5cf54c15edbfaac8e6e201db89f94a4746d3b13175cf8960d"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48f98be7724694687e4fc7e16870109ba6aacfc31aecca40ef927bdae0c339db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"16bbe36a02d0e3e8e9f01fb110230fbdacdb0707e98ee0a54a8eacd0d907051a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"37ab84231048f467c2b936aa5d14e2104c0d35e1ddc0abcb9e39891eaa640372"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2d354479ac28e801fc69ddbd171916a382bb4d9fe19b7d7e5e47b3993d775c29"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5289eb6c95e57d1ad45e356ff215b710b00977e6b878bac2f3666c337eff957c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3c2940471aea3e0ce4cb460fa12b31443bb9d2bcf6761b7e0d13334357c973b5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"04ec541aaf70d69c8653191b4536b5d179005c975df40ffcefa000785269d1b0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4c0c5a5528bebc895414052346322a486f3c13ea28ab92d8f3e6895440212bee"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"457d318a5dfcd57055dfe70341eb983de6d319e2865f580cd83838c1a35d0273"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"72f512e290a77f23ffb69963813bfc2889a0708c02478b163cf1ee0589c2c059"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"289bd7dd96c36c5a2cd05941aad06beda7473df7a2e09ecc0292ca3f4191ea7a"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2124419ef77feab36d68c55819258cd114a1ee496fdb8e247bb9002e403cb392"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5682cd00f9040a55c6f616db6ee35678fe44561fad92131a134ef9f6445c9e9b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4c279fbc9241e8ffc73217187428cbf39cb29dbc86a25a8bab3153146bc562bf"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"606022c0776c73bdba99263ba976580e3fa453dbc6ed3ce321495518cfac5c97"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1b3c85c78620750db6865c0ab7c02962e13bbafd26a8f62e680a06bf099e96a3"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4e88df3cf58488335c62ab4b60010b0ad3e7ec5c6fa8e1fbc819440076f39fb2"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2128334d48bb8c736754aec52e94822a683250edbca9c64bc71763b41cb5f0db"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"2efa47645723e1b1d2aba11a901c5ae5c6a3954f2da8143eda496296461638cb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0dedea303cf72dd88c5dd088f6b64a11ff0ad7e03808cc329077cbce08ccdcb0"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"70178e70b89186bff3ef1f778729fcf11e9909d1a341fc391cca61ae7503bea9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5506ba47f40b9142d10acfb4678db90e687060b17d1a11c3f07fa37eb759b2f9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"433bfa74fa0fe35dc664aa104f8618ab10eb6bb09d9e200e87a5aa78560d72fb"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5c141c77fd15a20905f40a4f46a47c782cd4b9b0246cddc79e1b5133721ec369"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"50425855791ad3327346bb7dd650c040a41e909d0e9e826d00850dcab138439b"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"0ab7145a5f012039efbd8abc425d1adc640625fe6ad438845382d418af87d0cc"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"52e36072d5fc448de5583f545c97443b9722b6fd380a0abf76bd27d629252b35"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"4513c98e9becd4d01b9d9734382c0116b59583474a487c2087da3c3498c1ffd9"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"37d2f2356a2f0eee375547232c36c6756249547d8c867acc93ec1963aa3c6d31"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"3c3fabeb32f89129ad9027840b32aa28686e0627135772c7b6b9510848947a00"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"48aa721a6af9f92c658a39617590780619dbefec5e31794a1358907354d5ff2c"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"5cc0ab5e75c80c54d6001fb4e90f302e3269c87afe4166f62fde2965e7fdf7c1"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"6abc22d42dfa2da89411aa00d98aac6a68d325dc34455a6c3cc98ceab56c5836"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"55f48be93ffd19a77df336ecf30080e84669468f92216bf063ed907addb560f5"},{"kind":"ReadScalar","tag":"dummy_eval","fe_be_hex":"1123b770b00b6e62a0f3c01c42d570127854425c70a66e004b6b7ebdeea69817"},{"kind":"Challenge","tag":"x1","fe_be_hex":"1c098ae6b043e3086251d5a69213433f867bce3584401f06632376bf4cc0d51f"},{"kind":"Challenge","tag":"x2","fe_be_hex":"450e371d88aee670e1855a0118a6ef70867a135a675e6283dc8a18cc1384add8"},{"kind":"ReadPoint","tag":"f_com","eip2537_hex":"909974aed62735634516ad3d04fb1c75814b1f04420e7ba5153e3f8ea730a425fd8cc7f4b9c06bc994f9b9fb18153ccd"},{"kind":"Challenge","tag":"x3","fe_be_hex":"2f336b860308ad449b601a9a51554f543475b39cd5647fee3fe190cf524b9693"},{"kind":"ReadScalar","tag":"q_eval","fe_be_hex":"3c84ab264411455250488b3d02d4498a82654a620a29d7f86a53d62c3b7cdd10"},{"kind":"Challenge","tag":"x4","fe_be_hex":"52e5456496a5abf0c1067996c62310f623e7d6b4972e9ae2746606965965c719"},{"kind":"ReadPoint","tag":"pi","eip2537_hex":"8b260bc8f81180fc5b6bce2401c2d81895ef4e29d8ac2300fad888e667cdbd286708865ad384a4aa27bd090951cf7660"}] \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/ivc/vk.bin b/proofs/solidity-verifier/fixtures/ivc/vk.bin deleted file mode 100644 index 670ff28e0..000000000 Binary files a/proofs/solidity-verifier/fixtures/ivc/vk.bin and /dev/null differ diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2Verifier.sol b/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2Verifier.sol new file mode 100644 index 000000000..713a6b431 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2Verifier.sol @@ -0,0 +1,3964 @@ +// SPDX-License-Identifier: CC0-1.0 +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; + +/// @title Halo2 BLS12-381 KZG verifier. +/// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 +/// proofs rendered by this repository's Rust generator. +/// @dev This contract ports the verifier flow from +/// `midfall/proofs/src/plonk/verifier.rs`, the Keccak transcript comments from +/// `midfall/proofs/src/transcript/implementors.rs`, and the KZG multi-open +/// comments from `midfall/proofs/src/poly/kzg/mod.rs`. +/// @dev It is not a generic verifier. The proof layout, VK payload, quotient +/// identity program, memory layout, and optional quotient evaluator are all +/// generated for one `VerifyingKey>`. +/// +/// Halo2 KZG verifier for the BLS12-381 curve, midnight-proofs flavour. +/// +/// Differences vs the original BN254 / halo2 v0.4 template: +// +/// - BLS12-381 base field Fp is 381 bits and does not fit in a uint256. +/// Each Fp coord is encoded EIP-2537 padded (16 zero bytes + 48 bytes). +/// A G1 point is 128 bytes (4 words); a G2 point is 256 bytes (8). +/// - Calldata carries G1 commitments in uncompressed EIP-2537 padded +/// form (4 words = 128 bytes per point: x_hi, x_lo, y_hi, y_lo). The +/// proof bytes produced by midnight-proofs prover are repacked off +/// chain (compressed -> uncompressed) before being passed to +/// `verifyProof`. The verifier hashes the uncompressed 128-byte form into +/// the transcript verbatim, matching `Hashable for G1Projective::to_input`; +/// see `common_uncompressed_g1`. +/// - Transcript `common` absorbs raw inputs in order. `squeeze` computes one +/// Keccak digest, resets the transcript buffer to that digest, then samples +/// by interpreting the digest as a big-endian integer modulo r. +/// - Scalar inversion uses modexp(scalar, r-2, r). +/// - Constructors run deployment-time smoke tests for MCOPY and the EIP-2537 +/// precompiles using identity inputs. Compile with Solidity >=0.8.24 and +/// deploy only on chains/forks that support MCOPY and EIP-2537. +contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + + + /// @notice Verifying-key contract address authorized for this verifier. + /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. + address public immutable AUTHORIZED_VK; + // Expected VK runtime metadata. The deployed VK runtime is + // INVALID || payload, hence EXPECTED_VK_LENGTH is one byte longer than + // EXPECTED_VK_PAYLOAD_LENGTH. + uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 17024; + uint256 internal constant EXPECTED_VK_LENGTH = 17025; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0xe68d89362065c8b7107055774f1e045b69bc3bffde4704541c5bc5c91c94cf52; + bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); + + // Solidity ABI calldata cursors. The generated verifier accepts exactly + // verifyProof(bytes proof, uint256[] instances), then parses the `proof` + // bytes itself in the same order as the Rust verifier transcript. + uint256 internal constant PROOF_LEN_CPTR = 0x44; + uint256 internal constant PROOF_CPTR = 0x64; + uint256 internal constant NUM_INSTANCE_CPTR = 0x1ec4; + uint256 internal constant INSTANCE_CPTR = 0x1ee4; + // First general-purpose memory words reserved by the generated verifier. + // RETURN_MPTR is a single word set to 1 on success. + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; + + // ---------------------------------------------------------------------- + // Verifying-key memory map. The VK header lives at VK_MPTR, followed + // by the quotient VM payload and commitments. After the full VK + // runtime comes the challenge slots (challenge_mptr..) and the + // per-stage scratch (theta_mptr..). + // ---------------------------------------------------------------------- + uint256 internal constant VK_MPTR = 0x3680; + uint256 internal constant VK_DIGEST_MPTR = 0x3680; + uint256 internal constant NUM_INSTANCES_MPTR = 0x36a0; + uint256 internal constant K_MPTR = 0x36c0; + uint256 internal constant N_INV_MPTR = 0x36e0; + uint256 internal constant OMEGA_MPTR = 0x3700; + uint256 internal constant OMEGA_INV_MPTR = 0x3720; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x3740; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x3760; + uint256 internal constant ACC_OFFSET_MPTR = 0x3780; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x37a0; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x37c0; + uint256 internal constant G1_BASE_MPTR = 0x37e0; + uint256 internal constant G2_BASE_MPTR = 0x3860; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x3960; + + uint256 internal constant CHALLENGE_MPTR = 0x7900; + + // Challenge layout. Squeeze order in midnight-proofs: + // user_phase challenges (variable count) + // theta -> beta, gamma -> trash_challenge -> y -> x -> + // x1, x2 -> x3 -> x4 + uint256 internal constant THETA_MPTR = 0x7900; + uint256 internal constant BETA_MPTR = 0x7920; + uint256 internal constant GAMMA_MPTR = 0x7940; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x7960; + uint256 internal constant Y_MPTR = 0x7980; + uint256 internal constant X_MPTR = 0x79a0; + uint256 internal constant X1_MPTR = 0x79c0; + uint256 internal constant X2_MPTR = 0x79e0; + uint256 internal constant X3_MPTR = 0x7a00; + uint256 internal constant X4_MPTR = 0x7a20; + + // Batch-open commitments live in 4-word EIP-2537 padded slots. + uint256 internal constant F_COM_MPTR = 0x7a40; + uint256 internal constant PI_MPTR = 0x7ac0; + + // Accumulator (KZG IVC). + uint256 internal constant ACC_LHS_MPTR = 0x7b40; + uint256 internal constant ACC_RHS_MPTR = 0x7bc0; + + // Lagrange / linearization scratch. + uint256 internal constant X_N_MPTR = 0x7c40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x7c60; + uint256 internal constant L_LAST_MPTR = 0x7c80; + uint256 internal constant L_BLIND_MPTR = 0x7ca0; + uint256 internal constant L_0_MPTR = 0x7cc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x7ce0; + // Legacy name: this is not h(x). It stores the expected opening + // scalar for the linearized commitment, i.e. the negated y-batched + // identity numerator reconstructed from the alleged evals at x. + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x7d00; + uint256 internal constant QUOTIENT_MPTR = 0x7d20; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x7dc0; + uint256 internal constant V_MPTR = 0x7de0; + uint256 internal constant FINAL_COM_MPTR = 0x7e00; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x7e80; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x7f00; // 4 words + + // Multi-prepare scratch (sized at codegen time). + uint256 internal constant ROT_POINTS_MPTR = 0x7f80; + uint256 internal constant X1_POWERS_MPTR = 0x8300; + // Q_COM materialization is currently fused into the final MSM scratch, + // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero + // reserved capacity until a future emitter starts writing Q_COM_MPTR. + uint256 internal constant Q_COM_MPTR = 0x8b20; + uint256 internal constant Q_EVAL_SET_MPTR = 0x8b20; + + // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals + // block of the proof; we keep it as a memory slot for symmetry. + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x9220; + + // Reserved 4-word slot for the G1 identity (point at infinity) in + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x9320; + + // Decoded polynomial-eval buffer (Optimisation H3). The off-chain + // Solidity proof shim rewrites proof scalars into canonical BE words, + // so `calldataload` gives the field element directly. The transcript- + // side `evaluations` loop range-checks and spills that value here so + // downstream eval references (gate evaluator + PCS q_eval Horner) + // become 3-gas `mload(...)` instead of calldata reads. + uint256 internal constant REVERSED_EVALS_MPTR = 0x9480; + uint256 internal constant SELECTOR_ACC_MPTR = 0xb140; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0xb140; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0xb580; + uint256 internal constant TRACE_U256_MPTR = 0xe340; + + // ---------------------------------------------------------------------- + // Per-category bases for EIP-2537 padded G1 commitments. The proof + // calldata carries 128-byte uncompressed/padded G1s after the off-chain + // proof shim repacks midnight-proofs' native compressed stream; this + // region stores the 4-word slots used by PCS / quotient-fold sections. + // + // Cumulative offsets (in words from `comms_mptr_base`): + // ADVICE_COMMS_MPTR_BASE + 0 + // LOOKUP_M_COMMS_MPTR_BASE + 4*total_advices + // PERM_Z_COMMS_MPTR_BASE + 4*total_advices + 4*num_lookups + // LOOKUP_HELPER_COMMS_MPTR_BASE + ... + 4*num_permutation_zs + // LOOKUP_Z_COMMS_MPTR_BASE + ... + 4*lookup_helper_chunks_total + // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups + // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans + // ---------------------------------------------------------------------- + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0xa140; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0xa8c0; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0xa9c0; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0xacc0; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0xadc0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0xaec0; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0xaf40; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 525096; + // Worst-case accumulator RHS MSM: carried RHS point plus every generated + // fixed-base tail scalar nonzero. Zero tail scalars are omitted at + // runtime, which only lowers the actual cost below this bound. + uint256 internal constant ACC_RHS_MSM_GAS = 12000; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x76eca8e8f19f0bb8c7d8d0b7757bf44687a635676d3a55e5ca8efb47b4481999; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; + + // BLS12-381 scalar-field modulus, used for transcript challenges and all + // Halo2 verifier arithmetic. + uint256 internal constant FR_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + // BLS12-381 Fp modulus minus one, split like an EIP-2537 coordinate: + // high word = 16 zero bytes || top 16 coordinate bytes, low word = + // bottom 32 coordinate bytes. + uint256 internal constant BLS_P_HI = 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7; + uint256 internal constant BLS_P_MINUS_ONE_LO = 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + // Packed public-accumulator sentinels for the shifted coordinate codec. + // The `_WITH_ID_FLAG` variant is used only for the first x-coordinate word. + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0 = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG = 0x00000000f38512bf6730d2a0f6b0f6241eabfffeb153ffffbafeffffffffaaaa; + uint256 internal constant BLS_P_MINUS_ONE_PACKED_1 = 0x0000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd764774b84; + + /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. + /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. + function require_eip2537_precompiles() private view { + assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + + // Scratch is reused for every runtime-prerequisite probe. + let scratch := 0x1000 + + // MCOPY must be available because the verifier uses it for + // proof-time point/scratch staging. Execute the opcode here so a + // non-Cancun fork fails during deployment instead of later proofs. + mstore(scratch, 0x1234) + mcopy(add(scratch, 0x20), scratch, 0x20) + if iszero(eq(mload(add(scratch, 0x20)), 0x1234)) { revert(0, 0) } + + // Start the EIP-2537 probes with the identity encoding for G1/G2: + // all-zero padded words. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + + // G1ADD(identity, identity) -> identity, 128-byte return. + // This catches chains where the precompile is missing or returns a + // non-standard success shape. + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + + // Worst-case generated G1MSM with all identity/zero terms -> + // identity, 128-byte return. This exercises the largest MSM input + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0xb140 + for { let off := 0 } lt(off, 0x30c0) { off := add(off, 0x20) } { + mstore(add(msm_scratch, off), 0) + } + // The production verifier uses G1MSM both for commitments and as + // the subgroup validator for absorbed proof points. + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x30c0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { + revert(0, 0) + } + + // PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)]) + // -> true, 32-byte return. This matches the runtime two-pair KZG + // pairing input size and catches absent pairing precompiles, + // short return data, and obviously incompatible semantics. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(scratch), 1)) { revert(0, 0) } + } + } + + + /// @notice Create a verifier pinned to a generated verifying key. + /// @dev Checks MCOPY/EIP-2537 availability and verifies the VK runtime before storing its address. + /// @param authorizedVk Address of the generated `Halo2VerifyingKey` runtime. + constructor(address authorizedVk) { + // Embedded quotient path: only the external VK runtime needs to be + // pinned, but the runtime opcode/precompile prerequisites are still + // mandatory. + require_eip2537_precompiles(); + require( + authorizedVk.code.length == EXPECTED_VK_LENGTH + && authorizedVk.codehash == EXPECTED_VK_CODEHASH, + "invalid vk" + ); + AUTHORIZED_VK = authorizedVk; + } + + /// @notice Verify a Halo2/Midfall proof for the generated verifying key. + /// @dev This checks only that `proof` verifies for the supplied public + /// `instances` under this pinned VK/protocol. Application contracts must + /// bind the meaning of those instances separately: state roots, program + /// identifiers, expected IVC outputs, chain/domain separation, and any + /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. + /// @dev Production renders are success-or-revert: accepted proofs return + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. + /// @dev The generated verifier uses absolute Yul memory addresses instead + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main + /// assembly block remains terminal: accepted proofs return from assembly + /// and all rejected inputs revert. Do not inline this body into Solidity + /// code that continues executing after verification without reviewing the + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. + /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. + /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. + /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. + function verifyProof( + bytes calldata proof, + uint256[] calldata instances + ) external view returns (bool) { + // Cheap ABI-shape guard before any generated memory work: + // - proof head must point at the bytes payload; + // - instances head must point at the generated instance array. + // + // The verifier below is a hand-rolled calldata parser. Failing here + // keeps malformed dynamic-argument layouts from being interpreted as a + // valid Midfall proof stream. + assembly ("memory-safe") { + if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) + } + } + // Non-embedded renders pin the VK by address and codehash. The Yul + // loader rechecks the runtime before every proof and copies the + // INVALID-prefixed payload into VK_MPTR. + address vk = AUTHORIZED_VK; + assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + + // This block owns the call-frame memory and remains terminal. + // Generated scratch starts at TRANSCRIPT_MPTR, preserving + // Solidity's reserved scratch, free-memory-pointer, and zero-slot + // words. See docs/architecture/MEMORY_LAYOUT.md. + // =============================================================== + // Helpers: modexp, transcript, EIP-2537 calls + // =============================================================== + + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // calls this only after transcript absorption is complete, so it + // reuses the dead transcript buffer just below VK_MPTR instead of + // a fixed post-VK address that can collide with live PCS scratch + // when the VK payload becomes smaller. + function scalar_inv(x) -> inv { + // Zero has no multiplicative inverse in Fr; callers rely on a + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x3580 + // EIP-198 modexp frame: + // [base_len, exp_len, mod_len, base, exponent, modulus] + mstore(add(p, 0x00), 0x20) // base len + mstore(add(p, 0x20), 0x20) // exp len + mstore(add(p, 0x40), 0x20) // mod len + mstore(add(p, 0x60), x) + mstore(add(p, 0x80), sub(FR_MODULUS, 2)) + mstore(add(p, 0xa0), FR_MODULUS) + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + inv := mload(p) + } + + // ---------- Streaming Keccak256 transcript helpers ---------- + // + // The transcript buffer lives at + // memory[TRANSCRIPT_MPTR..buf_len). On verifier entry it starts + // empty. Each common(input) appends raw bytes. squeeze_*(buf_len) + // computes one Keccak digest, reseeds the buffer with that + // 32-byte digest, and samples a Fq element as + // uint256(digest_be) mod r. + + function transcript_init() -> buf_len { + // Empty transcript buffer starts exactly at TRANSCRIPT_MPTR. + buf_len := TRANSCRIPT_MPTR + } + + // Append one 32-byte big-endian field/transcript word at the + // current end of the transcript buffer. + function common_word(buf_len, word) -> ret { + mstore(buf_len, word) + ret := add(buf_len, 32) + } + + // Absorb a BLS12-381 G1 point in EIP-2537 padded + // uncompressed form (4 calldata words = 128 bytes: + // x_hi || x_lo || y_hi || y_lo, each coord = 16 zero + // pad bytes + 48 big-endian field bytes) into the + // transcript buffer at `buf_len`. + // + // Matches the patched `Hashable for + // midnight_curves::G1Projective::to_input` in + // midnight-proofs, which now emits the same 128-byte form + // (`midfall/proofs/src/transcript/implementors.rs`). The + // previous emitter hashed the 48-byte ZCash compressed + // encoding instead and ran a 384-bit `lex(y) > lex(p − y)` + // ladder + identity flag fixup to derive the sign bit on + // the fly; switching to the uncompressed form drops that + // ladder entirely. + // + // Canonicality: reject non-zero bytes in the top 16 bytes + // of each `_hi` calldata word and reject coordinates + // outside Fp. Normalizing those bytes before hashing would + // make multiple calldata encodings share one transcript. + // + // This helper does not run an independent curve/subgroup + // check. Instead, ProtocolPlan::validate rejects generated + // plans where an absorbed proof commitment would not later be + // consumed by an EIP-2537 G1MSM or pairing path, and those + // precompiles perform the curve/subgroup validation. + // + // The point's uncompressed form remains in calldata; the + // call site is responsible for `calldatacopy`-ing it into + // memory afterwards if it needs the on-curve coordinates. + function common_uncompressed_g1(buf_len, cptr) -> ret { + let x_hi_word := calldataload(cptr) + let x_lo := calldataload(add(cptr, 0x20)) + let y_hi_word := calldataload(add(cptr, 0x40)) + let y_lo := calldataload(add(cptr, 0x60)) + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + + let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) + let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) + if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { + fail(ERR_BAD_POINT_ENCODING) + } + if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { + fail(ERR_BAD_POINT_ENCODING) + } + + // Memcpy the 4 calldata words (128 bytes) verbatim + // into the keccak buffer. + calldatacopy(buf_len, cptr, 0x80) + ret := add(buf_len, 0x80) + } + + // One Keccak finalization + reseed. Returns the new buffer + // cursor (= TRANSCRIPT_MPTR + 32) and stores the squeezed Fq at + // `mptr`. + function squeeze_to(buf_len, mptr) -> ret { + let h0 := keccak256(TRANSCRIPT_MPTR, sub(buf_len, TRANSCRIPT_MPTR)) + // Reseed: write the 32-byte digest at start of buffer. + mstore(TRANSCRIPT_MPTR, h0) + let r := FR_MODULUS + // Sample Fq as uint256(keccak_digest_be) mod r. + mstore(mptr, mod(h0, r)) + ret := add(TRANSCRIPT_MPTR, 32) + } + + // ---------- EC primitives (EIP-2537 wrappers) ---------- + // + // These mirror the BN254 helpers but operate on 4-word G1 + // points. They use planned memory windows above Solidity's + // reserved prefix; the streaming transcript buffer is no longer + // needed once all challenges are squeezed. + + // Invert a contiguous run of Fr words in-place using Montgomery's + // batch inversion trick: + // 1. write prefix products to scratch; + // 2. invert the total product once with modexp; + // 3. walk backward to recover each individual inverse. + // + // The function returns a boolean instead of reverting so callers + // can combine it with other `success` plumbing until a section + // boundary decides whether to fail closed. + function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { + ret := success + if iszero(ret) { leave } + // Memory ranges must be forward and word-aligned by + // construction; a reversed range is always a codegen error. + if lt(mptr_end, mptr_start) { + ret := 0 + leave + } + + let count_bytes := sub(mptr_end, mptr_start) + // Empty batch is valid and leaves memory untouched. + if iszero(count_bytes) { leave } + + // Fast path for a single denominator: avoid prefix scratch and + // just run one modexp inverse in place. + if eq(count_bytes, 0x20) { + let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } + if iszero(x) { + ret := 0 + leave + } + + let single_scratch := scratch_mptr + mstore(add(single_scratch, 0x00), 0x20) + mstore(add(single_scratch, 0x20), 0x20) + mstore(add(single_scratch, 0x40), 0x20) + mstore(add(single_scratch, 0x60), x) + mstore(add(single_scratch, 0x80), sub(r, 2)) + mstore(add(single_scratch, 0xa0), r) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + if ret { mstore(mptr_start, mload(single_scratch)) } + leave + } + + // Forward pass: scratch stores prefix products up to, but not + // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. + let gp_mptr := scratch_mptr + let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } + let mptr := add(mptr_start, 0x20) + for {} lt(mptr, sub(mptr_end, 0x20)) {} { + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) + mstore(gp_mptr, gp) + mptr := add(mptr, 0x20) + gp_mptr := add(gp_mptr, 0x20) + } + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) + // A zero total product means at least one denominator was + // zero, so no batch inverse exists. + if iszero(gp) { + ret := 0 + leave + } + + // Invert the total product once. + mstore(add(gp_mptr, 0x00), 0x20) + mstore(add(gp_mptr, 0x20), 0x20) + mstore(add(gp_mptr, 0x40), 0x20) + mstore(add(gp_mptr, 0x60), gp) + mstore(add(gp_mptr, 0x80), sub(r, 2)) + mstore(add(gp_mptr, 0xa0), r) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } + let all_inv := mload(gp_mptr) + + // Backward pass: derive each inverse from the inverted total + // product and the saved prefix products. + let first_mptr := mptr_start + let second_mptr := add(first_mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + for {} lt(second_mptr, mptr) {} { + let inv := mulmod(all_inv, mload(gp_mptr), r) + all_inv := mulmod(all_inv, mload(mptr), r) + mstore(mptr, inv) + mptr := sub(mptr, 0x20) + gp_mptr := sub(gp_mptr, 0x20) + } + let inv_first := mulmod(all_inv, mload(second_mptr), r) + let inv_second := mulmod(all_inv, mload(first_mptr), r) + mstore(first_mptr, inv_first) + mstore(second_mptr, inv_second) + } + + // Final EIP-2537 pairing wrapper. `lhs_mptr` and `rhs_mptr` are + // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. + function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { + ret := success + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } + // Lay out two (G1, G2) pairs at scratch..scratch+0x300: + // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] + // Cancun MCOPY (3 + 3·words gas) replaces what used to + // be a 4-step mstore chain for each G1 (~60 gas) and an + // 8-iter mstore loop for each G2 (~240 gas). Net saving + // here is ~500 gas per ec_pairing call. + let scratch := 0x1240 + mcopy(scratch, lhs_mptr, 0x80) + mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) + mcopy(add(scratch, 0x180), rhs_mptr, 0x80) + mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) + ret := and(ret, eq(returndatasize(), 0x20)) + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } + ret := 1 + } + + // ---------- IVC accumulator public-input decoding ---------- + // + // `AssignedForeignPoint` exposes each base-field coordinate + // through `AssignedField::as_public_input`: seven radix-2^56 limbs of + // (coord - 1) are packed four-at-a-time into native field elements. + // The x coordinate's first packed word carries the identity flag by + // adding one raw radix base. Rebuild EIP-2537 padded + // (x_hi, x_lo, y_hi, y_lo) words from that encoding. + // + // Public-input layout for one coordinate: + // word 0: limb_0 | limb_1 << bits | ... up to limbs_per_word + // word 1: next limbs, if any + // + // The limbs are little-endian in the represented integer even + // though calldata words are loaded as big 256-bit values. The loop + // below extracts each limb by shifting inside the packed word and + // reconstructs the full coordinate into the two-word EIP-2537 + // representation expected by the BLS12-381 precompiles. + function load_acc_coord_shifted(src, bits, n, base, limbs_per_word, first_adjust) -> hi, lo { + // Mask for one radix limb, e.g. 2^56 - 1 for the current + // BLS12-381 self-emulation parameters. + let mask := sub(base, 1) + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + // Limb words are little-endian packed inside each Fr + // public input. `first_adjust` removes the identity flag + // base from the first x word when present. + let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { + packed := sub(packed, first_adjust) + } + // Select limb i from its packed field word. The mod/div + // pair maps a limb index to an intra-word limb slot and + // the calldata word containing it. + let limb := and(shr(mul(mod(i, limbs_per_word), bits), packed), mask) + + let shift := mul(i, bits) + // Split the reconstructed 384-bit coordinate into the + // EIP-2537 high/low words expected by the precompiles. + if lt(shift, 256) { + lo := add(lo, shl(shift, limb)) + if gt(add(shift, bits), 256) { + // A limb can straddle the 256-bit low/high split. + // Move the overflow bits into hi. + hi := add(hi, shr(sub(256, shift), limb)) + } + } + if iszero(lt(shift, 256)) { + // Once shift >= 256 the whole limb belongs to hi. + hi := add(hi, shl(sub(shift, 256), limb)) + } + } + } + + // The shifted coordinate codec represents zero as p-1 before the + // final +1 below, so keep this sentinel explicit. + function is_bls_p_minus_one(hi, lo) -> yes { + yes := and(eq(hi, BLS_P_HI), eq(lo, BLS_P_MINUS_ONE_LO)) + } + + // Canonical encoded accumulator identity: + // x = p-1 plus the identity flag in the first packed word, + // y = p-1 with no identity flag. + // It decodes to the EIP-2537 point-at-infinity slot (all zeros). + // + // This fast path is deliberately stricter than "decodes to zero": + // the point at infinity has exactly one accepted public-input + // encoding. Non-canonical zero-like encodings are rejected later. + function is_acc_encoded_identity(src) -> yes { + yes := and( + and( + eq(calldataload(src), BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG), + eq(calldataload(add(src, 0x20)), BLS_P_MINUS_ONE_PACKED_1) + ), + and( + eq(calldataload(add(src, 0x40)), BLS_P_MINUS_ONE_PACKED_0), + eq(calldataload(add(src, 0x60)), BLS_P_MINUS_ONE_PACKED_1) + ) + ) + } + + // Reject unused high bits in the packed public-input words. This + // makes each accumulator point encoding canonical before it reaches + // the precompile-based curve/subgroup validation. + function check_acc_coord_packing(src, bits, n, limbs_per_word) -> ok { + ok := 1 + // Number of packed native-field public-input words occupied by + // one coordinate. + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + for { let word_idx := 0 } lt(word_idx, coord_words) { word_idx := add(word_idx, 1) } { + // The final word may contain fewer than limbs_per_word + // limbs. Any unused high bits must be zero, otherwise the + // same coordinate would have multiple calldata encodings. + let remaining := sub(n, mul(word_idx, limbs_per_word)) + let limbs_in_word := limbs_per_word + if lt(remaining, limbs_per_word) { + limbs_in_word := remaining + } + let used_bits := mul(limbs_in_word, bits) + if lt(used_bits, 256) { + // shl(used_bits, 1) == 2^used_bits. The packed word + // must be strictly less than that bound. + ok := and(ok, lt(calldataload(add(src, mul(word_idx, 0x20))), shl(used_bits, 1))) + } + } + } + + // Decode one shifted coordinate. `allow_id` is true only for x, + // because the identity flag lives in x's first packed word. + function load_acc_coord(src, allow_id, bits, n, base, limbs_per_word) -> ok, hi, lo, is_id { + ok := check_acc_coord_packing(src, bits, n, limbs_per_word) + if and(allow_id, iszero(lt(calldataload(src), base))) { + // Probe the x identity flag by removing one radix base and + // checking whether the adjusted coordinate is p-1. + // + // `calldataload(src) >= base` is a cheap prefilter: only x + // can carry this flag, and adding one radix base must make + // the first packed word at least base. + let adj_hi, adj_lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, base) + is_id := is_bls_p_minus_one(adj_hi, adj_lo) + } + + // Decode again with the identity adjustment applied only when + // the canonical identity flag was actually detected. + hi, lo := load_acc_coord_shifted(src, bits, n, base, limbs_per_word, mul(is_id, base)) + ok := and( + ok, + // Coordinate must be in the BLS12-381 base field, i.e. + // <= p - 1 in split hi/lo form. + or(lt(hi, BLS_P_HI), and(eq(hi, BLS_P_HI), iszero(gt(lo, BLS_P_MINUS_ONE_LO)))) + ) + + let was_p_minus_one := is_bls_p_minus_one(hi, lo) + if was_p_minus_one { + // Shifted encoding maps p-1 back to zero. + hi := 0 + lo := 0 + } + if iszero(was_p_minus_one) { + // All other coordinates are encoded as coord - 1, so add + // one back with carry into the high word. + let next_lo := add(lo, 1) + hi := add(hi, lt(next_lo, lo)) + lo := next_lo + } + + // EIP-2537 pads each 48-byte Fp coordinate to 64 bytes, + // so the high word must fit in its low 128 bits. + // This also catches impossible reconstructions above 384 bits. + ok := and(ok, lt(hi, shl(128, 1))) + } + + // Decode a public accumulator point into an EIP-2537 4-word G1 + // slot. Non-identity points are curve/subgroup checked later by + // routing them through G1MSM. + function load_acc_point(dst, src, bits, n, base) -> ok, is_id { + // Prefer the canonical all-coordinate identity encoding before + // attempting coordinate-level shifted decoding. This accepts + // the point at infinity only in the exact form generated by the + // circuit's public-input codec. + is_id := is_acc_encoded_identity(src) + if is_id { + ok := 1 + // EIP-2537 encodes G1 identity as four zero words: + // x_hi = x_lo = y_hi = y_lo = 0. + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // x occupies coord_words packed public-input words; y + // starts immediately after x. + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // Only x may carry the identity flag. y must decode as a + // normal shifted coordinate. + let x_ok, x_hi, x_lo, x_is_id := load_acc_coord(src, 1, bits, n, base, limbs_per_word) + let y_ok, y_hi, y_lo, y_id := load_acc_coord( + add(src, mul(coord_words, 0x20)), + 0, + bits, + n, + base, + limbs_per_word + ) + // y_id is always zero because allow_id was false, but the + // tuple shape is shared with x decoding. + pop(y_id) + ok := and(x_ok, y_ok) + is_id := x_is_id + + if is_id { + // If x carried the identity flag, both decoded + // coordinates must be zero after shifting. Any other y + // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. + ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) + mstore(dst, 0) + mstore(add(dst, 0x20), 0) + mstore(add(dst, 0x40), 0) + mstore(add(dst, 0x60), 0) + } + if iszero(is_id) { + // The coordinate codec maps encoded p-1 to decoded + // zero. EIP-2537 reserves affine (0,0) for the point + // at infinity, so a decoded infinity is only valid + // when the canonical accumulator identity encoding + // was used above. + let decoded_zero := iszero(or(or(x_hi, x_lo), or(y_hi, y_lo))) + ok := and(ok, iszero(decoded_zero)) + // Store the affine point in the exact precompile input + // layout: x_hi, x_lo, y_hi, y_lo. + mstore(dst, x_hi) + mstore(add(dst, 0x20), x_lo) + mstore(add(dst, 0x40), y_hi) + mstore(add(dst, 0x60), y_lo) + } + } + } + // Validate and prepare the public accumulator equation before the + // main transcript starts. This fails malformed public inputs early + // and writes ACC_LHS_MPTR / ACC_RHS_MPTR for final pairing batching. + // + // The accumulator public input represents an equality of two G1 + // commitments used by the recursive KZG accumulator. This helper: + // 1. decodes carried public G1 points from shifted limbs; + // 2. forces every decoded point through EIP-2537 G1MSM so the + // precompile validates curve/subgroup membership; + // 3. folds the RHS carried point and fixed-base scalar tail into + // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for + // randomized batching in FinalPairing.yul. + // `r` is consumed only by the canonicality guards in the + // carried-scalar and fixed-base-tail arms; renders whose + // accumulator layout has neither (e.g. point_pair with no tail) + // legally leave it unused. + function validate_public_accumulator(success, r) -> out { + out := success + let bits := 56 + let n := 7 + // The BLS12-381 self-emulation currently exposes Fp + // coordinates as 7 radix-2^56 limbs. + let limb_base := shl(bits, 1) + let limbs_per_word := 4 + let coord_words := div(add(n, sub(limbs_per_word, 1)), limbs_per_word) + // acc_offset is generated from the VK/protocol shape and + // points into the ABI `instances` array. + let acc_instance_ptr := add(INSTANCE_CPTR, 0x0160) + + // LHS layout: point limbs (x,y), then either an explicit + // scalar word or an implicit unit scalar for already-collapsed + // point-pair public inputs. + // The scalar pointer is computed unconditionally; the rendered + // branch below decides whether to read it or use scalar 1. + let lhs_scalar_ptr := add(acc_instance_ptr, mul(mul(2, coord_words), 0x20)) + let lhs_ok, lhs_is_id := load_acc_point(ACC_LHS_MPTR, acc_instance_ptr, bits, n, limb_base) + out := and(out, lhs_ok) + // Shared scratch for one-pair LHS validation and the later + // variable-length RHS MSM. + let acc_scratch := 0xb140 + { + // Already-collapsed point-pair layout: carried scalars are + // implicit one. + let lhs_scalar := 1 + // Identity status is useful for decoding checks above, but + // validation still goes through G1MSM for all points. + pop(lhs_is_id) + // Always route the decoded carried point through G1MSM, + // even for identity points and zero/one scalars. The + // precompile is the on-curve/subgroup validator for this + // public-input point; skipping it would let a malformed + // non-identity point hide behind scalar 0. + mcopy(acc_scratch, ACC_LHS_MPTR, 0x80) + mstore(add(acc_scratch, 0x80), lhs_scalar) + if out { + // Single-pair MSM output overwrites ACC_LHS_MPTR with + // lhs_scalar * decoded_lhs. If lhs_scalar is one, this + // is also a curve/subgroup validation round-trip. + out := staticcall(G1MSM_GAS_1PAIR, 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // RHS layout for this generated verifier is an already + // collapsed point pair: lhs point, rhs point. Both carried + // scalars are implicit one, and there is no fixed-base scalar + // tail. + let rhs_instance_ptr := lhs_scalar_ptr + // RHS scalar, when present, immediately follows the RHS point + // limbs. The fixed-base scalar tail starts after it. + let rhs_scalar_ptr := add(rhs_instance_ptr, mul(mul(2, coord_words), 0x20)) + let rhs_ok, rhs_is_id := load_acc_point(ACC_RHS_MPTR, rhs_instance_ptr, bits, n, limb_base) + out := and(out, rhs_ok) + // acc_pair_ptr appends (G1, scalar) pairs into acc_scratch for + // one final RHS MSM. + let acc_pair_ptr := acc_scratch + { + // Implicit unit scalar for already-collapsed point pairs. + let rhs_scalar := 1 + pop(rhs_is_id) + // Keep the carried RHS point in the MSM input even when + // it is encoded as identity or has scalar 0/1, so EIP-2537 + // validates every decoded public accumulator point before + // it can affect, or be erased from, the pairing batch. + mcopy(acc_pair_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(acc_pair_ptr, 0x80), rhs_scalar) + // Move to the next (G1, scalar) pair slot. + acc_pair_ptr := add(acc_pair_ptr, 0xa0) + } + // Total byte length of the appended RHS MSM input pairs. This + // is at least one pair because the carried RHS point is always + // appended; keep the guard for synthetic render configurations. + let acc_msm_len := sub(acc_pair_ptr, acc_scratch) + if acc_msm_len { + // Fold the carried RHS point and any generated fixed-base + // tail into ACC_RHS_MPTR. The later final pairing block + // randomizes this equation together with the KZG pairing. + if out { + // Output overwrites ACC_RHS_MPTR with: + // rhs_scalar * carried_rhs + // + sum_i fixed_scalar_i * fixed_base_i + // + // The precompile also validates every nonzero fixed + // base embedded by codegen and the carried RHS point. + // ACC_RHS_MSM_GAS is the compile-time worst case + // (every tail scalar nonzero); acc_msm_len can only + // select a same-size-or-smaller MSM at runtime. + out := staticcall( + ACC_RHS_MSM_GAS, + 0x0c, + acc_scratch, + acc_msm_len, + ACC_RHS_MPTR, + 0x80 + ) + out := and(out, eq(returndatasize(), 0x80)) + } + } + // The caller checks `out` and reverts before transcript work if + // any decode, canonicality, or precompile validation failed. + } + + + + let r := FR_MODULUS + let success := true + + + + // =============================================================== + // VK loading: either bake in the embedded VK bytes or fetch + // them from the linked AUTHORIZED_VK contract. + // + // This is the first verifier phase after helper definitions. Its + // job is to make the generated VK payload available at VK_MPTR in + // one canonical memory layout, regardless of whether this render + // embeds the VK directly or links a separate Halo2VerifyingKey + // contract. + // + // Later template partials treat VK_MPTR as already populated with: + // - header words: vk_digest, domain data, accumulator metadata; + // - BLS12-381 base points used by the final pairing; + // - compact quotient VM constants/program bytes, when enabled; + // - fixed and permutation commitments in 4-word G1 slots. + // =============================================================== + { + // Re-check the pinned VK dependency on every proof. The + // constructor check catches normal deployment mistakes, while + // this fresh check hardens forks or same-transaction edge + // cases where code at the authorized address could differ + // from the runtime originally pinned by this verifier. + // + // EXPECTED_VK_LENGTH includes the leading INVALID byte in the + // Halo2VerifyingKey runtime. EXPECTED_VK_CODEHASH_WORD is the + // full runtime hash, not only the payload hash. + if iszero(and( + eq(extcodesize(vk), EXPECTED_VK_LENGTH), + eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) + )) { fail(ERR_VK_MISMATCH) } + // Runtime byte 0 is INVALID so direct calls cannot execute the + // payload. Copy from byte 1 into VK_MPTR to reconstruct the + // exact payload layout used by the embedded branch. + extcodecopy(vk, VK_MPTR, 0x01, EXPECTED_VK_PAYLOAD_LENGTH) + + // Cross-check loaded VK header words against the verifier + // constants used by later parser, domain, and accumulator + // paths. Codehash pinning protects the external VK address; + // these checks catch generator drift before calldata parsing + // chooses a stale schema. + success := and(success, eq(mload(NUM_INSTANCES_MPTR), 19)) + success := and(success, eq(mload(K_MPTR), 20)) + success := and(success, eq(mload(HAS_ACCUMULATOR_MPTR), 1)) + success := and(success, eq(mload(ACC_OFFSET_MPTR), 11)) + success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 7)) + success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) + if iszero(success) { fail(ERR_VK_MISMATCH) } + // + // The checks below validate the dynamic ABI envelope before the + // transcript parser starts walking raw calldata: + // - proof bytes length equals the generated proof layout; + // - instance array length equals the generated public input + // count; + // - total calldata length has no missing or trailing words. + // + // `success` is folded through `and` for consistency with later + // sections, then immediately enforced at the end of this block. + // A failure here means the verifier is not looking at the proof + // shape it was generated to parse. + success := and(success, eq(0x1e60, calldataload(PROOF_LEN_CPTR))) + success := and(success, eq(19, calldataload(NUM_INSTANCE_CPTR))) + // Calldata must contain exactly the ABI selector, proof bytes, + // instance-array length, and generated number of instance + // words. Any trailing bytes fail closed. + success := and( + success, + eq(calldatasize(), add(INSTANCE_CPTR, 0x0260)) + ) + // Stop before any transcript absorption if the ABI/proof shape + // is not exactly the generated one. + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } + } + // Fail malformed accumulator public inputs before transcript, + // quotient, PCS, and final pairing work. The late accumulator block + // only batches these already-validated G1 outputs into the final + // pairing equation. + // + // Accumulator validation decodes shifted public-input limbs into + // EIP-2537 G1 slots, checks canonical encodings, and routes points + // through G1MSM for curve/subgroup validation. Doing it here means + // invalid accumulator public inputs cannot influence transcript + // challenge derivation or waste gas in later quotient/PCS work. + // validate_public_accumulator returns a boolean to share the same + // success-plumbing style as other helper calls; this boundary is + // where the verifier converts failure to a revert. + success := validate_public_accumulator(success, r) + if iszero(success) { fail(ERR_BAD_POINT_ENCODING) } + + // =============================================================== + // Transcript: VK digest + instances + proof. + // + // This block is the Solidity mirror of the native Midfall verifier + // transcript schedule. It does three jobs at once: + // + // 1. Absorb public data and proof bytes into the streaming + // Keccak transcript in exactly the native order. + // 2. Decode/range-check proof scalars and canonical G1 calldata. + // 3. Copy proof commitments/evaluations into planned memory + // slots consumed by Lagrange, quotient, PCS, and pairing + // blocks later in the verifier. + // + // `buf_len` is a write cursor into the transcript buffer. The + // helper functions append bytes and return the new cursor; squeeze + // helpers hash memory[TRANSCRIPT_MPTR..buf_len), reseed the buffer + // with the digest, and write the sampled Fr challenge to memory. + // =============================================================== + let buf_len := transcript_init() + // VK_DIGEST_MPTR holds the digest as a BE 32-byte word (the + // VK contract stores it via `mstore`, which matches the + // Keccak Fq transcript input). + // + // This digest commits to the verifier key / constraint system + // before any proof material is read. + buf_len := common_word(buf_len, mload(VK_DIGEST_MPTR)) + + // Absorb committed_pi = G1Affine::identity() when the + // `committed-instances` feature is on in midnight-proofs. + // Under the patched `Hashable::to_input` (see + // `midfall/proofs/src/transcript/implementors.rs`), the + // identity hashes as 128 zero bytes (EIP-2537 (0,0) + // convention), NOT the 48-byte ZCash compressed form + // 0xc0||47*0x00 that the previous emitter produced. + // Native verifier absorbs this BEFORE the instance count. + { + // 128 zero bytes: zero out 4 consecutive 32-byte words + // at buf_len. + // This is a raw transcript absorb, not a memory slot kept for + // later elliptic-curve operations. + mstore(buf_len, 0) + mstore(add(buf_len, 0x20), 0) + mstore(add(buf_len, 0x40), 0) + mstore(add(buf_len, 0x60), 0) + buf_len := add(buf_len, 0x80) + } + + { + // Native verifier absorbs a length scalar before instance + // values; Keccak Fq transcript input is canonical BE. + // The ABI length was already checked against this generated + // constant in VkLoading.yul. + buf_len := common_word(buf_len, 19) + + let instance_cptr := INSTANCE_CPTR + for { let instance_cptr_end := add(instance_cptr, 0x0260) } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) } { + let inst_be := calldataload(instance_cptr) + // Public inputs are BLS12-381 scalar-field elements. They + // must be canonical before transcript absorption; accepting + // non-canonical encodings would admit transcript aliases. + success := and(success, lt(inst_be, r)) + // Instances are passed BE in calldata, matching the + // Keccak Fq transcript input. + buf_len := common_word(buf_len, inst_be) + } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } + } + + // =============================================================== + // Per-user-phase reads + challenge squeezes. + // + // Each proof G1 is already EIP-2537 padded in calldata. The + // verifier validates and absorbs that 128-byte form, then copies + // it into the corresponding per-category MPTR. The PCS / + // quotient-fold blocks below dereference those MPTRs. + // + // All G1 reads follow the same pattern: + // - common_uncompressed_g1 canonicalizes/range-checks the two Fp + // coordinates and appends the exact 128 calldata bytes; + // - calldatacopy stores the same 4-word G1 slot in planned + // memory for later EIP-2537 precompile calls; + // - proof_cptr advances by one G1 byte length. + // =============================================================== + // proof_cptr walks the raw proof bytes inside the ABI `bytes` + // payload. Every successful read advances it exactly once, and the + // final equality check below proves the parser consumed the whole + // generated proof layout. + let proof_cptr := PROOF_CPTR + // advice_walk mirrors proof commitment order into the contiguous + // G1 commitment memory region used by PCS and quotient folding. + let advice_walk := ADVICE_COMMS_MPTR_BASE + // ---- User phase 1 ---- + // Advice commitments for this phase are absorbed before the phase's + // challenge squeezes. The number of commitments and challenges is + // generated from the protocol plan. + for { let end := add(proof_cptr, 0x0780) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + // Store the commitment at its phase-ordered advice slot. + calldatacopy(advice_walk, proof_cptr, 0x80) + advice_walk := add(advice_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + + // ---- theta ---- + // From this point onward the transcript alternates between + // squeezed challenges and proof commitments exactly as + // midnight-proofs does in `plonk/verifier.rs`. + // theta batches lookup input expressions. + buf_len := squeeze_to(buf_len, THETA_MPTR) + // ---- multiplicities (one G1 per lookup) ---- + // Lookup multiplicity commitments are absorbed after theta and + // copied into their own contiguous G1 region. + let lookup_m_walk := LOOKUP_M_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0100) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_m_walk, proof_cptr, 0x80) + lookup_m_walk := add(lookup_m_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + + // ---- beta, gamma ---- + // beta and gamma are the permutation/lookup randomizers. They are + // squeezed after lookup multiplicities and before permutation + // product commitments, matching the native verifier schedule. + buf_len := squeeze_to(buf_len, BETA_MPTR) + buf_len := squeeze_to(buf_len, GAMMA_MPTR) + // ---- permutation Z products ---- + // Permutation product commitments are used by the permutation + // identities in the quotient numerator and later by PCS openings. + let perm_z_walk := PERM_Z_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0300) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(perm_z_walk, proof_cptr, 0x80) + perm_z_walk := add(perm_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // ---- lookup helpers + accumulators (per-lookup) ---- + // Each lookup contributes zero or more helper commitments followed + // by its lookup accumulator Z commitment. The generated layout keeps + // helper commitments and accumulator commitments in separate memory + // regions because the quotient/PCS schedules address them + // differently. + let lookup_helper_walk := LOOKUP_HELPER_COMMS_MPTR_BASE + let lookup_z_walk := LOOKUP_Z_COMMS_MPTR_BASE + // lookup 0: 1 helper(s) + 1 acc + // Helper commitments for lookup 0. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 0. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + // lookup 1: 1 helper(s) + 1 acc + // Helper commitments for lookup 1. + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_helper_walk, proof_cptr, 0x80) + lookup_helper_walk := add(lookup_helper_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + // Accumulator commitment for lookup 1. This is + // always one G1 when the lookup section is present. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(lookup_z_walk, proof_cptr, 0x80) + lookup_z_walk := add(lookup_z_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // ---- trash_challenge ---- + // Midnight squeezes this challenge unconditionally, even when the + // circuit has no trash arguments. + // Keeping this squeeze unconditional preserves transcript + // compatibility across circuits with and without trash columns. + buf_len := squeeze_to(buf_len, TRASH_CHALLENGE_MPTR) + // ---- trashcans ---- + // Trashcan commitments are optional, but when present they are + // absorbed before y so the quotient batching challenge binds them. + let trashcan_walk := TRASHCAN_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x80) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(trashcan_walk, proof_cptr, 0x80) + trashcan_walk := add(trashcan_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + + // ---- y ---- + // y batches all quotient identities. Quotient commitments are read + // only after y is sampled, matching the Rust verifier flow. + buf_len := squeeze_to(buf_len, Y_MPTR) + + // ---- quotient commitment(s) ---- + // Each uncompressed quotient commitment is calldatacopied directly to + // QUOTIENT_LIMB_COMMS_MPTR_BASE; the Horner fold below reads + // them back from memory. common_uncompressed_g1 absorbs the + // 128-byte calldata form into the transcript verbatim. + // + // Multi-limb quotient mode reads several Q_i commitments; single-H + // mode renders this loop with one limb. + let quotient_walk := QUOTIENT_LIMB_COMMS_MPTR_BASE + for { let end := add(proof_cptr, 0x0200) } + lt(proof_cptr, end) + {} { + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(quotient_walk, proof_cptr, 0x80) + quotient_walk := add(quotient_walk, 0x80) + proof_cptr := add(proof_cptr, 0x80) + } + + // ---- x ---- + // x is the main evaluation point. Values read after this point are + // alleged polynomial evaluations at x or derived PCS openings. + buf_len := squeeze_to(buf_len, X_MPTR) + + // ---- evaluations ---- + // Optimisation H3: the off-chain Solidity proof shim rewrites + // proof scalars into BE calldata words. Spill each decoded eval + // into REVERSED_EVALS_MPTR in the same iteration we range-check + // it, so downstream references can use cheap mload. + // + // The Rust verifier conceptually reads evaluations in query order. + // The lowering plan arranges REVERSED_EVALS_MPTR in the order used + // by the quotient VM/direct evaluator, hence the generated name. + { + let eval_buf := REVERSED_EVALS_MPTR + for { let end := add(proof_cptr, 0x0cc0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Proof evaluation scalars must be canonical Fr elements + // before they are absorbed or made available to quotient + // reconstruction. + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } + // Spill for quotient numerator and PCS codegen. + mstore(eval_buf, eval) + eval_buf := add(eval_buf, 0x20) + // Absorb the exact BE field word used by the native + // Keccak transcript. + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + } + + // ---- x1, x2 ---- + // x1 and x2 batch the KZG multi-opening reduction. They are + // squeezed after all polynomial evaluations are absorbed. + buf_len := squeeze_to(buf_len, X1_MPTR) + buf_len := squeeze_to(buf_len, X2_MPTR) + + // ---- f_com (1 uncompressed G1) ---- + // f_com is the commitment to the batched polynomial used by the PCS + // multi-open protocol. It is both transcript material and later + // pairing/MSM input. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(F_COM_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // ---- x3 ---- + // x3 is the PCS evaluation point for f_com. + buf_len := squeeze_to(buf_len, X3_MPTR) + // truncated-challenges mirrors midnight-proofs + // proofs/src/poly/kzg/mod.rs: + // - x3 is the f_com evaluation point and is truncated + // immediately after squeeze. + // - x1 and x4 remain full squeezed Fr words, but later PCS + // batching stores truncate(x1^i) and truncate(x4^i) while + // keeping the internal power accumulators full precision. + // This direct x3 mask is therefore one part of the PCS truncation + // rule, not the only truncated value used by the verifier. + mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) + + // ---- q_evals (one Fq per point set) ---- + // q_evals are not spilled into REVERSED_EVALS_MPTR because the PCS + // emitter reads them as a contiguous calldata range from the saved + // Q_EVAL_CPTR_MPTR cursor. + // + // Each q_eval is the claimed evaluation for one prepared point set + // in the KZG multi-open reduction. They are still transcript + // material and must be range-checked as Fr scalars. + mstore(Q_EVAL_CPTR_MPTR, proof_cptr) + for { let end := add(proof_cptr, 0xa0) } + lt(proof_cptr, end) + {} { + let eval := calldataload(proof_cptr) + // Canonical Fr check before transcript absorption. + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } + buf_len := common_word(buf_len, eval) + proof_cptr := add(proof_cptr, 0x20) + } + + // ---- x4 ---- + // x4 is the final PCS batching challenge, sampled after q_evals + // and before the opening proof point pi. + buf_len := squeeze_to(buf_len, X4_MPTR) + + // ---- pi (1 uncompressed G1) ---- + // pi is the KZG opening proof commitment. It is the last proof + // object absorbed into the transcript and later becomes one side of + // the final pairing check. + buf_len := common_uncompressed_g1(buf_len, proof_cptr) + calldatacopy(PI_MPTR, proof_cptr, 0x80) + proof_cptr := add(proof_cptr, 0x80) + + // The hand-rolled proof parser must consume exactly the ABI + // `proof` bytes before the `instances` length word. This is + // redundant with the generated proof length today, but makes + // future proof-layout drift fail closed. + // + // NUM_INSTANCE_CPTR is the calldata word immediately after the + // dynamic proof bytes payload. If proof_cptr lands anywhere else, + // some section was under-read or over-read. + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } + + // `success` carries deferred canonicality failures from public + // instance reads. G1/proof scalar helpers revert immediately. + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } + + // =============================================================== + // Lagrange & instance-evaluation block (pure Fr arithmetic). + // =============================================================== + { + let k := 20 + let x := mload(X_MPTR) + // Compute x^n by repeated squaring, with n = 2^k. + let x_n := x + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_n := mulmod(x_n, x_n, r) + } + + let omega := mload(OMEGA_MPTR) + + // First pass writes denominators (x - omega_i) for every + // Lagrange value needed below, then appends x^n - 1. The + // batch inversion pass turns all of them into inverses in one + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR + let mptr_end := add(mptr, 0x03a0) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, addmod(x, sub(r, pow_of_omega), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + let x_n_minus_1 := addmod(x_n, sub(r, 1), r) + mstore(mptr_end, x_n_minus_1) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + + // Convert inverted denominators into Lagrange evaluations: + // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). + mptr := LAGRANGE_DENOMS_MPTR + let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) + for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } + lt(mptr, mptr_end) + { mptr := add(mptr, 0x20) } { + mstore(mptr, mulmod(l_i_common, mulmod(mload(mptr), pow_of_omega, r), r)) + pow_of_omega := mulmod(pow_of_omega, omega, r) + } + + // l_blind is the sum of the negative-rotation Lagrange terms + // used by the midnight-proofs blinding identity. + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0140) } + lt(l_i_cptr, l_i_cptr_end) + { l_i_cptr := add(l_i_cptr, 0x20) } { + l_blind := addmod(l_blind, mload(l_i_cptr), r) + } + + // Public instance polynomial evaluation at x. Instance words + // have already been range-checked and absorbed in transcript + // order; this loop only forms the linear combination. + let instance_eval := 0 + for { + let instance_cptr := INSTANCE_CPTR + let instance_cptr_end := add(instance_cptr, 0x0260) + } + lt(instance_cptr, instance_cptr_end) + { instance_cptr := add(instance_cptr, 0x20) + l_i_cptr := add(l_i_cptr, 0x20) } { + instance_eval := addmod(instance_eval, mulmod(mload(l_i_cptr), calldataload(instance_cptr), r), r) + } + + // Persist the derived values into named memory slots consumed + // by quotient reconstruction and PCS preparation. + let x_n_minus_1_inv := mload(mptr_end) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0140)) + + mstore(X_N_MPTR, x_n) + mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) + mstore(L_LAST_MPTR, l_last) + mstore(L_BLIND_MPTR, l_blind) + mstore(L_0_MPTR, l_0) + mstore(INSTANCE_EVAL_MPTR, instance_eval) + } + + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } + + // Optional quotient helper functions. Each one is rendered only + // when the Rust lowering pass recognized the corresponding + // expression shape in this generated verifier. They are pure Fr + // helpers and share the same FR_MODULUS as the surrounding + // numerator block. + // VK-specialized identity helper for Poseidon S-box terms. + // + // Rust source shape: + // circuits/src/hash/poseidon/poseidon_chip.rs::sbox + // full_round_gate / partial_round_gate + // circuits/src/hash/poseidon/round_skips.rs::RoundId + // + // The Rust verifier only sees this as an Expression tree from + // `vk.cs.gates`; the generator emits q_pow5 after recognizing five + // equal multiplicative factors. It is a codegen shortcut for x^5, + // not a separate verifier rule. + function q_pow5(x) -> z { + let q_r := FR_MODULUS + let x2 := mulmod(x, x, q_r) + z := mulmod(x, mulmod(x2, x2, q_r), q_r) + } + // =============================================================== + // Batched identity numerator / linearization target. + // + // This block does not evaluate the quotient polynomial h(x), and + // the proof does not provide an h(x) scalar to trust. Instead it: + // + // 1. Reconstructs the y-batched constraint numerator nu_y(x) + // from the alleged polynomial evaluations read after the + // transcript sampled x. + // 2. Stores -nu_y(x) as the expected opening scalar for the + // linearized commitment. + // + // The commitment side is built in the next block from the quotient + // limb commitments as (1 - x^n) * Σ_i x_split^i * Q_i, plus any + // simple-selector commitments. The PCS check later binds that + // linearized commitment to this expected scalar at x. + // + // Rust source-of-truth: + // - verifier.rs reads quotient commitments, samples x, then + // reads/computes all evaluations used below. + // - mod.rs::partially_evaluate_identities returns identities in + // gate, permutation, lookup, trash order. + // - linearization/verifier.rs::compute_linearization_commitment + // reverse-folds those identities by powers of y, sends + // simple-selector identities to selector commitment scalars, + // and subtracts fully-evaluated identities into expected_eval. + // + // This template is shared by the monolithic and external quotient + // paths. In the external path, Halo2QuotientEvaluator first copies + // the verifier memory frame into the same generated addresses. + // + // Runtime inputs expected to exist before this block starts: + // - `r` is the BLS12-381 scalar-field modulus. + // - Y_MPTR holds the quotient batching challenge y. + // - X_MPTR, L_*_MPTR, INSTANCE_EVAL_MPTR, and + // REVERSED_EVALS_MPTR hold values parsed or derived by the + // main verifier after the transcript sampled x. + // - VK_MPTR holds the pinned VK payload; in compact mode that + // payload includes the quotient constant table and bytecode. + // + // Runtime outputs written by this block: + // - QUOTIENT_EVAL_MPTR receives the scalar expected opening for + // the linearized commitment, namely -nu_y(x). + // - SELECTOR_ACC_MPTR[0..num_simple_selectors) receives one + // linearization scalar per generated simple selector. + // + // Line-by-line reading conventions used below: + // + // * Every runtime value is one canonical Fr element stored in a + // 256-bit EVM memory word. The small integer operands decoded + // from q_program are never field values; they are pointers, + // constant-table slots, selector indexes, offsets, or counts. + // + // * `mload(ptr)` is the only way the VM turns a small pointer + // operand into a real 255-bit field element. The value loaded + // from memory is then combined with `addmod(..., r)` or + // `mulmod(..., r)`, so every arithmetic line is reduced modulo + // the BLS12-381 scalar-field order. + // + // * `q_top` is the cached top of the VM operand stack. When an + // opcode needs to push while `q_top` is already live, the old + // value is written to `q_sp` and `q_sp` is advanced by one + // word. Binary `ADD`/`MUL` move `q_sp` back by one word and + // combine that spilled value with `q_top`. + // + // * Identity boundaries are explicit. Expression opcodes leave + // one value in `q_top`; `FOLD_MAIN` or `FOLD_SELECTOR` consumes + // it and advances the global y-batch position. Native callback + // opcodes are only emitted at empty-stack boundaries and run + // generated Yul that performs the same fold side effects. + // + // * The generated Solidity source intentionally emits comments + // before opcode cases. Those comments are documentation only: + // they do not affect bytecode, but they make rendered verifier + // assembly readable without jumping back to Rust codegen. + // =============================================================== + { + // Compact quotient-program mode. + // + // The largest identity expressions are not all emitted as + // unrolled Yul. Instead, most arithmetic is encoded as a small + // q_program bytecode stored in the VK payload. This block + // interprets that program, while selected heavy identities may + // still be emitted as native callbacks for gas. + // + // Compact mode is a code-size trade: short bytecode operands + // name already-planned memory slots, and the interpreter turns + // those names into Fr arithmetic. The opcode stream is fully + // generated and pinned by the VK/runtime codehash; no proof + // calldata can alter control flow. + // Load the quotient batching challenge used by every fold. + let y := mload(Y_MPTR) + + // q_const_mptr points to Fr constants used by the VM. + // q_program_mptr points to the bytecode stream. + // Constants are stored as consecutive 32-byte Fr words. + let q_const_mptr := 0x3a60 + // Program bytes are also stored in the VK payload, packed into + // 32-byte words by PackedProgramCodec. + let q_program_mptr := 0x50a0 + // Running Horner accumulator for fully evaluated identities. + // After all identities, this is nu_y(x) for the `None` + // identity group. + // Initialize A = 0 before scanning the identity stream. + mstore(0xb280, 0) + // Simple selectors are grouped into separate linearization + // buckets. They start at zero for every proof. + // q_sel_zero_off walks selector bucket byte offsets. + for { let q_sel_zero_off := 0 } lt(q_sel_zero_off, 0x0140) { q_sel_zero_off := add(q_sel_zero_off, 0x20) } { + // B_s = 0 for each simple selector bucket. + mstore(add(SELECTOR_ACC_MPTR, q_sel_zero_off), 0) + } + // Codegen knows the selector identity positions. Precompute + // the y^k powers needed for selector gap and tail updates, + // avoiding a runtime y^-1 modexp and per-identity selector + // scale maintenance. + { + // q_y_power holds y^i at the current loop index. + let q_y_power := 1 + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0xb2c0, 1) + // Start at i=1 because y^0 = 1 is written above. + for { let q_y_power_i := 1 } lt(q_y_power_i, 49) { q_y_power_i := add(q_y_power_i, 1) } { + // Advance from y^(i-1) to y^i modulo Fr. + q_y_power := mulmod(q_y_power, y, r) + // Store y^i at selector_power_mptr + 32*i. + mstore(add(0xb2c0, shl(5, q_y_power_i)), q_y_power) + } + } + + // Direct inline prefix. These identities are generated as Yul + // before entering the VM. They use the same fold snippets as + // VM/native identities, so they occupy the same y-batch order. + { + let var0 := 0x1 + let f_3 := mload(0x9ac0) + let f_4 := mload(0x99c0) + let a_0 := mload(0x94a0) + let var1 := mulmod(f_4, a_0, r) + let var2 := addmod(f_3, var1, r) + let f_5 := mload(0x99e0) + let a_1 := mload(0x94c0) + let var3 := mulmod(f_5, a_1, r) + let var4 := addmod(var2, var3, r) + let f_6 := mload(0x9a00) + let a_2 := mload(0x94e0) + let var5 := mulmod(f_6, a_2, r) + let var6 := addmod(var4, var5, r) + let f_7 := mload(0x9a20) + let a_3 := mload(0x9500) + let var7 := mulmod(f_7, a_3, r) + let var8 := addmod(var6, var7, r) + let f_8 := mload(0x9a40) + let a_4 := mload(0x9520) + let var9 := mulmod(f_8, a_4, r) + let var10 := addmod(var8, var9, r) + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var11 := mulmod(f_0, a_0_next_1, r) + let var12 := addmod(var10, var11, r) + let f_1 := mload(0x9a80) + let var13 := mulmod(f_1, a_0, r) + let var14 := mulmod(var13, a_1, r) + let var15 := addmod(var12, var14, r) + let f_2 := mload(0x9aa0) + let var16 := mulmod(f_2, a_0, r) + let var17 := mulmod(var16, a_2, r) + let var18 := addmod(var15, var17, r) + let var19 := mulmod(var0, var18, r) + mstore(0xb8e0, var19) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x94c0) + let a_2 := mload(0x94e0) + let var1 := addmod(a_1, a_2, r) + let a_3 := mload(0x9500) + let var2 := addmod(0, sub(r, a_3), r) + let var3 := addmod(var1, var2, r) + let a_4 := mload(0x9520) + let var4 := addmod(0, sub(r, a_4), r) + let var5 := addmod(var3, var4, r) + let var6 := mulmod(var0, var5, r) + mstore(0xb8e0, var6) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let f_4 := mload(0x99c0) + let var1 := addmod(a_0, f_4, r) + let a_0_next_1 := mload(0x9540) + let var2 := addmod(0, sub(r, a_0_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xb8e0, var4) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + { + let var0 := 0x1 + let a_1 := mload(0x94c0) + let f_5 := mload(0x99e0) + let var1 := addmod(a_1, f_5, r) + let a_1_next_1 := mload(0x9560) + let var2 := addmod(0, sub(r, a_1_next_1), r) + let var3 := addmod(var1, var2, r) + let var4 := mulmod(var0, var3, r) + mstore(0xb8e0, var4) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + + // VM registers: + // q_pc current bytecode pointer + // q_end end of bytecode stream + // q_sp memory stack pointer for non-top stack values + // q_top cached top-of-stack value + // q_has_top whether q_top currently holds a stack value + // + // The cached top reduces memory traffic in the interpreter. + // q_sp's registered range must cover the interpreted operand + // stack plus any native callback scratch that reuses this base + // pointer. In particular, the native permutation callback + // writes a structured scratch table at program.stack_mptr. + // q_pc starts at the first encoded instruction. + let q_pc := q_program_mptr + // q_end is an exclusive byte pointer for the VM loop. + let q_end := add(q_program_mptr, 0x11cf) + // q_sp starts at the first free stack word. + let q_sp := 0xb8e0 + // q_top is meaningless until q_has_top is set. + let q_top := 0 + // q_has_top = 0 means the VM stack is empty. + let q_has_top := 0 + + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity + // 0x21 modarith7 + // + // The default IVC verifier uses one physical encoding for the + // logical VM: compact byte-oriented opcodes with variable-width + // operands, dynamic runs, and limb-aware cases. + + // Byte-oriented encoding: opcodes are one byte followed by + // variable-width operand bytes. + for { } lt(q_pc, q_end) { } { + // The bytecode table is byte-addressed, but EVM memory + // loads whole words. `byte(0, mload(q_pc))` extracts the + // opcode at the current byte cursor; each case advances + // q_pc by exactly its operand width. + let q_op := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + + switch q_op + // VM 0x05 PUSH_MEM_U16 (bytes): next two bytes are a short memory pointer. + case 0x05 { + // Operand layout: u16 absolute memory pointer. The + // memory planner keeps the hot quotient frame below + // 64 KiB when this compact form is emitted. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + q_top := mload(q_ptr) + q_has_top := 1 + } + // VM 0x06 ADD: pop one spilled stack word and add it to q_top. + case 0x06 { + // The safety validator guarantees a spilled operand + // exists before ADD. q_top is the right operand. + if eq(q_sp, 0xb8e0) { q_program_fail() } + q_sp := sub(q_sp, 0x20) + q_top := addmod(mload(q_sp), q_top, r) + } + // VM 0x08 NEG: replace q_top with its Fr negation. + case 0x08 { + // addmod(0, r - x, r) maps zero back to zero and every + // nonzero scalar to its canonical additive inverse. + q_top := addmod(0, sub(r, q_top), r) + } + // VM 0x0d MUL_CONST_U8: multiply q_top by a small constant-table slot. + case 0x0d { + // One-byte constant-index multiply, used by short + // affine chains after an initial PUSH. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) + } + // VM 0x10 ADD_MEM_U16: add a short memory load into q_top. + case 0x10 { + // Operand layout: u16 pointer. The pointed word is an + // already range-checked Fr scalar in verifier memory. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_top := addmod(q_top, mload(q_ptr), r) + } + // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. + case 0x11 { + // In-place multiply by a planned memory word. + let q_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_top := mulmod(q_top, mload(q_ptr), r) + } + // Limb-aware opcodes are opt-in compact forms for + // structurally recognized non-SHA foreign-field shapes. + // Coefficients are indexes into q_const_mptr, which is + // generated from VK/program data, never from proof + // calldata. + // + // Rust source shape: + // proofs/src/plonk/mod.rs::partially_evaluate_identities + // circuits/src/field/foreign/util.rs::{sum_exprs,pair_wise_prod} + // circuits/src/field/foreign/params.rs::{base_powers,double_base_powers} + // + // "Foreign field" means the circuit represents elements + // modulo another modulus m as 7 limbs in base + // 2^LOG2_BASE. The verifier does not switch fields; it + // evaluates the lowered identity over BLS12-381 Fr, using + // Fr coefficients equal to base^i mod m or base^(i+j) mod m. + // VM 0x21 MODARITH7: byte-only fused affine 7-limb foreign-field/ECC identity. + case 0x21 { + // MODARITH7: + // maybe_cond * ( + // c + // + sum LIN7 blocks + // + sum BILIN7_ROW blocks + // + sum BILIN7_PAIRWISE blocks + // + sum coeff[k] * mload(ptr[k]) + // + sum coeff[k] * mload(lhs[k]) * mload(rhs[k]) + // ) + // It is a dispatch/operand-load optimization only; + // all coefficients still come from the generated + // quotient constant table. + // + // Flags: + // bit 0: multiply the final affine sum by a memory + // condition word. + // bit 1: seed q_acc from a constant-table word + // before reading the counted term blocks. + let q_flags := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + let q_cond_ptr := 0 + if and(q_flags, 0x01) { + // Optional condition pointer. When present, the + // whole identity is gated by mload(q_cond_ptr). + q_cond_ptr := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_cond_ptr, 0x3680), 0x6aa0) { q_program_fail() } + } + + let q_acc := 0 + if and(q_flags, 0x02) { + // Optional constant seed for affine identities + // with a standalone constant term. + let qconst := byte(0, mload(q_pc)) + q_pc := add(q_pc, 1) + q_acc := mload(add(q_const_mptr, shl(5, qconst))) + } + + // Five one-byte counters describe the blocks that + // follow. Each block has a fixed-width internal layout, + // so q_pc can advance without per-term tags. + let q_counts_word := mload(q_pc) + let q_lin_count := byte(0, q_counts_word) + let q_row_count := byte(1, q_counts_word) + let q_pairwise_count := byte(2, q_counts_word) + let q_mem_count := byte(3, q_counts_word) + let q_product_count := byte(4, q_counts_word) + q_pc := add(q_pc, 5) + + if q_has_top { + mstore(q_sp, q_top) + q_sp := add(q_sp, 0x20) + } + + // LIN7 blocks: q_acc += sum_i c_i * limb_i. + for { let q_lin_block := 0 } lt(q_lin_block, q_lin_count) { q_lin_block := add(q_lin_block, 1) } { + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + } + + // BILIN7_ROW blocks: q_acc += lhs * sum_i c_i * rhs_i. + for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { + let q_lhs := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } + let q_lhs_value := mload(q_lhs) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_rhs := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + + // BILIN7_PAIRWISE blocks: q_acc += weighted 7-by-7 + // product convolution. + for { let q_pair_block := 0 } lt(q_pair_block, q_pairwise_count) { q_pair_block := add(q_pair_block, 1) } { + let q_pair_word := mload(q_pc) + let q_lhs_base := shr(240, q_pair_word) + let q_rhs_base := and(shr(224, q_pair_word), 0xffff) + q_pc := add(q_pc, 0x04) + if gt(sub(q_lhs_base, 0x3680), 0x69e0) { q_program_fail() } + if gt(sub(q_rhs_base, 0x3680), 0x69e0) { q_program_fail() } + let q_coeff_pc := q_pc + q_pc := add(q_pc, 13) + for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { + let q_lhs_value := mload(add(q_lhs_base, shl(5, q_i))) + for { let q_j := 0 } lt(q_j, 7) { q_j := add(q_j, 1) } { + let qconst := byte(0, mload(add(q_coeff_pc, add(q_i, q_j)))) + q_acc := addmod( + q_acc, + mulmod( + mulmod(q_lhs_value, mload(add(q_rhs_base, shl(5, q_j))), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + } + } + + // Extra linear memory terms outside the 7-limb shapes. + for { let q_mem_block := 0 } lt(q_mem_block, q_mem_count) { q_mem_block := add(q_mem_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_ptr := and(shr(232, q_word), 0xffff) + q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), + r + ) + } + + // Extra binary product terms outside the 7-limb shapes. + for { let q_product_block := 0 } lt(q_product_block, q_product_count) { q_product_block := add(q_product_block, 1) } { + let q_word := mload(q_pc) + let qconst := byte(0, q_word) + let q_lhs := and(shr(232, q_word), 0xffff) + let q_rhs := and(shr(216, q_word), 0xffff) + q_pc := add(q_pc, 5) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } + q_acc := addmod( + q_acc, + mulmod( + mulmod(mload(q_lhs), mload(q_rhs), r), + mload(add(q_const_mptr, shl(5, qconst))), + r + ), + r + ) + } + + if and(q_flags, 0x01) { + // Apply the optional gate condition last so every + // subterm shares the same selector/condition. + q_acc := mulmod(mload(q_cond_ptr), q_acc, r) + } + // MODARITH7 pushes its fused identity value. + q_top := q_acc + q_has_top := 1 + } + // Native permutation callback. It evaluates the + // permutation identities from permutation.rs at this exact + // VM position, preserving the Rust identity order while + // avoiding a large interpreted product loop. + // VM 0x19 NATIVE_PERMUTATION: marker for the generated permutation callback. + case 0x19 { + // Native callbacks are identity-boundary opcodes. They + // must not inherit any partially evaluated VM stack + // state from the previous expression. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // its scratch-table base, not as a conventional VM + // stack. The Rust memory planner must reserve enough + // words for structured_permutation_scratch_words(meta) + // whenever this opcode can appear. + q_sp := 0xb8e0 + // The generated lines below call the same fold snippets + // used by interpreted expressions, so trace IDs and + // y-batch positions remain contiguous. + { + let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 + let q_perm_vals := 0xb8e0 + let q_perm_sigmas := 0xbb20 + let q_perm_z_cur := 0xbd60 + let q_perm_z_next := 0xbe20 + let q_perm_z_last := 0xbee0 + let q_perm_delta_base_ptr := 0xbf80 + let q_perm_num_cols := 18 + let q_perm_num_sets := 6 + let q_perm_chunk_len := 3 + let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 + mstore(add(q_perm_vals, 0x0), mload(0x99a0)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x94a0, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0xc0), mload(0x9480)) + mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) + { + for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 9) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { + let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) + let q_perm_val_load_src_off := q_perm_val_load_dst_off + mstore(add(add(q_perm_vals, 0x100), q_perm_val_load_dst_off), mload(add(0x95a0, q_perm_val_load_src_off))) + } + } + mstore(add(q_perm_vals, 0x220), mload(0x9980)) + { + for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 18) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { + let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) + let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x9bc0, q_perm_sigma_load_src_off))) + } + } + { + for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 6) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { + let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) + let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x9e00, q_perm_z_cur_load_src_off))) + } + } + { + for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 6) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { + let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) + let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x9e20, q_perm_z_next_load_src_off))) + } + } + { + for { let q_perm_z_last_load_i := 0 } lt(q_perm_z_last_load_i, 5) { q_perm_z_last_load_i := add(q_perm_z_last_load_i, 1) } { + let q_perm_z_last_load_dst_off := shl(5, q_perm_z_last_load_i) + let q_perm_z_last_load_src_off := mul(q_perm_z_last_load_i, 0x60) + mstore(add(add(q_perm_z_last, 0x0), q_perm_z_last_load_dst_off), mload(add(0x9e40, q_perm_z_last_load_src_off))) + } + } + let q_perm_eval := 0 + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + let q_perm_zn := mload(add(q_perm_z_cur, 0xa0)) + q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + for { let q_perm_i := 1 } lt(q_perm_i, 6) { q_perm_i := add(q_perm_i, 1) } { + let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) + let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) + q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + } + mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) + for { let q_perm_set := 0 } lt(q_perm_set, 6) { q_perm_set := add(q_perm_set, 1) } { + let q_perm_start := mul(q_perm_set, q_perm_chunk_len) + let q_perm_end := add(q_perm_start, q_perm_chunk_len) + if gt(q_perm_end, q_perm_num_cols) { q_perm_end := q_perm_num_cols } + let q_perm_left := mload(add(q_perm_z_next, shl(5, q_perm_set))) + let q_perm_right := mload(add(q_perm_z_cur, shl(5, q_perm_set))) + let q_perm_delta_pow := mload(q_perm_delta_base_ptr) + for { let q_perm_j := q_perm_start } lt(q_perm_j, q_perm_end) { q_perm_j := add(q_perm_j, 1) } { + let q_perm_off := shl(5, q_perm_j) + let q_perm_v := mload(add(q_perm_vals, q_perm_off)) + let q_perm_s := mload(add(q_perm_sigmas, q_perm_off)) + q_perm_left := mulmod(q_perm_left, addmod(addmod(q_perm_v, mulmod(mload(BETA_MPTR), q_perm_s, r), r), mload(GAMMA_MPTR), r), r) + q_perm_right := mulmod(q_perm_right, addmod(addmod(q_perm_v, q_perm_delta_pow, r), mload(GAMMA_MPTR), r), r) + q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) + } + q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) + mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) + } + } + } + // Native lookup callback. This whole-family opcode + // evaluates the LogUp boundary, helper-chunk, and + // accumulator identities at this VM position, preserving + // the Rust y-batch order while avoiding many interpreted + // product-loop opcodes. + // VM 0x1f NATIVE_LOOKUP: marker for the generated LogUp lookup callback. + case 0x1f { + // Reset VM stack state before entering structured + // lookup Yul. Lookup callbacks own their scratch + // layout and perform all needed folds internally. + q_top := 0 + q_has_top := 0 + // The generated loop below uses program.stack_mptr as + // f+beta/prefix/suffix scratch rather than as a + // conventional VM stack. The Rust memory planner must + // reserve structured_lookup_scratch_words(meta). + q_sp := 0xb8e0 + // Generated LogUp code follows the same y-batch order + // as the Rust identity stream. + { + let q_lookup_f := 0xb8e0 + let q_lookup_prefix := 0xb960 + let q_lookup_suffix := 0xb9e0 + let q_lookup_l0 := mload(L_0_MPTR) + let q_lookup_llast := mload(L_LAST_MPTR) + let q_lookup_lblind := mload(L_BLIND_MPTR) + let q_lookup_lsum := addmod(q_lookup_l0, q_lookup_llast, r) + let q_lookup_active := addmod(1, sub(r, addmod(q_lookup_llast, q_lookup_lblind, r)), r) + let q_lookup_beta := mload(BETA_MPTR) + let q_lookup_theta := mload(THETA_MPTR) + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa060), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let f_10 := mload(0x9ae0) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) + let var1 := mulmod(var0, q_lookup_theta, r) + for { let q_lookup_shared_i := 0 } lt(q_lookup_shared_i, 4) { q_lookup_shared_i := add(q_lookup_shared_i, 1) } { + let q_lookup_shared_off := shl(5, q_lookup_shared_i) + let q_lookup_shared_tail := mload(add(0x94c0, q_lookup_shared_off)) + let q_lookup_shared_compressed := addmod(var1, q_lookup_shared_tail, r) + mstore(add(q_lookup_f, q_lookup_shared_off), addmod(q_lookup_shared_compressed, q_lookup_beta, r)) + } + let q_lookup_product := 1 + for { let q_lookup_prod_i := 0 } lt(q_lookup_prod_i, 4) { q_lookup_prod_i := add(q_lookup_prod_i, 1) } { + q_lookup_product := mulmod(q_lookup_product, mload(add(q_lookup_f, shl(5, q_lookup_prod_i))), r) + } + mstore(q_lookup_prefix, 1) + for { let q_lookup_pref_i := 1 } lt(q_lookup_pref_i, 4) { q_lookup_pref_i := add(q_lookup_pref_i, 1) } { + let q_lookup_pref_prev := sub(q_lookup_pref_i, 1) + mstore(add(q_lookup_prefix, shl(5, q_lookup_pref_i)), mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_pref_prev))), mload(add(q_lookup_f, shl(5, q_lookup_pref_prev))), r)) + } + mstore(add(q_lookup_suffix, 0x60), 1) + for { let q_lookup_suf_i := sub(4, 1) } gt(q_lookup_suf_i, 0) { q_lookup_suf_i := sub(q_lookup_suf_i, 1) } { + let q_lookup_suf_prev := sub(q_lookup_suf_i, 1) + mstore(add(q_lookup_suffix, shl(5, q_lookup_suf_prev)), mulmod(mload(add(q_lookup_suffix, shl(5, q_lookup_suf_i))), mload(add(q_lookup_f, shl(5, q_lookup_suf_i))), r)) + } + let q_lookup_sum := 0 + for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 4) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { + q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) + } + let q_lookup_eval := addmod(mulmod(mload(0xa040), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0xa040) + let f_17 := mload(0x9b60) + let f_11 := mload(0x9b00) + let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) + let f_12 := mload(0x9b20) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) + let q_lookup_s_sum_h := mulmod(f_17, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0xa080), sub(r, addmod(mload(0xa060), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa020), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + } + { + { + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa0e0), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let a_14 := mload(0x9980) + let var0 := addmod(mulmod(0, q_lookup_theta, r), a_14, r) + let a_0 := mload(0x94a0) + let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_0, r) + let a_1 := mload(0x94c0) + let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_1, r) + let a_2 := mload(0x94e0) + let var3 := addmod(mulmod(var2, q_lookup_theta, r), a_2, r) + let a_3 := mload(0x9500) + let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_3, r) + let a_4 := mload(0x9520) + let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_4, r) + let a_5 := mload(0x95a0) + let var6 := addmod(mulmod(var5, q_lookup_theta, r), a_5, r) + let a_6 := mload(0x95c0) + let var7 := addmod(mulmod(var6, q_lookup_theta, r), a_6, r) + let a_7 := mload(0x95e0) + let var8 := addmod(mulmod(var7, q_lookup_theta, r), a_7, r) + let a_8 := mload(0x9600) + let var9 := addmod(mulmod(var8, q_lookup_theta, r), a_8, r) + let a_9 := mload(0x9620) + let var10 := addmod(mulmod(var9, q_lookup_theta, r), a_9, r) + let a_10 := mload(0x9640) + let var11 := addmod(mulmod(var10, q_lookup_theta, r), a_10, r) + let a_11 := mload(0x9660) + let var12 := addmod(mulmod(var11, q_lookup_theta, r), a_11, r) + let a_12 := mload(0x9680) + let var13 := addmod(mulmod(var12, q_lookup_theta, r), a_12, r) + let a_13 := mload(0x96a0) + let var14 := addmod(mulmod(var13, q_lookup_theta, r), a_13, r) + let f_13 := mload(0x9b40) + let var15 := addmod(mulmod(var14, q_lookup_theta, r), f_13, r) + let q_lookup_eval := addmod(mulmod(mload(0xa0c0), addmod(var15, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + { + let q_lookup_sum_h := mload(0xa0c0) + let var0 := 0x1 + let f_24 := mload(0x9b80) + let var1 := addmod(0, sub(r, f_24), r) + let var2 := addmod(var0, var1, r) + let a_14 := mload(0x9980) + let var3 := mulmod(var2, a_14, r) + let var4 := addmod(mulmod(0, q_lookup_theta, r), var3, r) + let a_0 := mload(0x94a0) + let var5 := mulmod(var2, a_0, r) + let var6 := addmod(mulmod(var4, q_lookup_theta, r), var5, r) + let a_1 := mload(0x94c0) + let var7 := mulmod(var2, a_1, r) + let var8 := addmod(mulmod(var6, q_lookup_theta, r), var7, r) + let a_2 := mload(0x94e0) + let var9 := mulmod(var2, a_2, r) + let var10 := addmod(mulmod(var8, q_lookup_theta, r), var9, r) + let a_3 := mload(0x9500) + let var11 := mulmod(var2, a_3, r) + let var12 := addmod(mulmod(var10, q_lookup_theta, r), var11, r) + let a_4 := mload(0x9520) + let var13 := mulmod(var2, a_4, r) + let var14 := addmod(mulmod(var12, q_lookup_theta, r), var13, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var2, a_5, r) + let var16 := addmod(mulmod(var14, q_lookup_theta, r), var15, r) + let a_6 := mload(0x95c0) + let var17 := mulmod(var2, a_6, r) + let var18 := addmod(mulmod(var16, q_lookup_theta, r), var17, r) + let a_7 := mload(0x95e0) + let var19 := mulmod(var2, a_7, r) + let var20 := addmod(mulmod(var18, q_lookup_theta, r), var19, r) + let a_8 := mload(0x9600) + let var21 := mulmod(var2, a_8, r) + let var22 := addmod(mulmod(var20, q_lookup_theta, r), var21, r) + let a_9 := mload(0x9620) + let var23 := mulmod(var2, a_9, r) + let var24 := addmod(mulmod(var22, q_lookup_theta, r), var23, r) + let a_10 := mload(0x9640) + let var25 := mulmod(var2, a_10, r) + let var26 := addmod(mulmod(var24, q_lookup_theta, r), var25, r) + let a_11 := mload(0x9660) + let var27 := mulmod(var2, a_11, r) + let var28 := addmod(mulmod(var26, q_lookup_theta, r), var27, r) + let a_12 := mload(0x9680) + let var29 := mulmod(var2, a_12, r) + let var30 := addmod(mulmod(var28, q_lookup_theta, r), var29, r) + let a_13 := mload(0x96a0) + let var31 := mulmod(var2, a_13, r) + let var32 := addmod(mulmod(var30, q_lookup_theta, r), var31, r) + let f_13 := mload(0x9b40) + let var33 := mulmod(var2, f_13, r) + let var34 := addmod(mulmod(var32, q_lookup_theta, r), var33, r) + let q_lookup_s_sum_h := mulmod(var0, q_lookup_sum_h, r) + let q_lookup_diff := addmod(mload(0xa100), sub(r, addmod(mload(0xa0e0), q_lookup_s_sum_h, r)), r) + let q_lookup_t_beta := addmod(var34, q_lookup_beta, r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa0a0), r) + let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) + } + } + } + } + // Native callbacks are generated only for the heaviest + // recognized Midfall gate identities. All other gate and + // non-native identity arithmetic remains in + // the compact q_program VM above, preserving the Rust + // `partially_evaluate_identities` order. + // VM 0x1b NATIVE_IDENTITY: marker for generated heavy-gate callbacks. + case 0x1b { + // Operand layout: u16 native callback index. The + // manifest validates that callback indexes appear in + // generated order and target existing switch cases. + let q_native_idx := shr(240, mload(q_pc)) + q_pc := add(q_pc, 2) + // Heavy identities are whole expressions, so clear the + // interpreter stack before dispatching. + q_top := 0 + q_has_top := 0 + q_sp := 0xb8e0 + // Native identity sub-cases are generated from selected heavy gate identities. + switch q_native_idx + case 0 { + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let a_0_next_1 := mload(0x9540) + let var1 := mulmod(a_0, a_0_next_1, r) + let var2 := 0x100000000000000 + let a_1_next_1 := mload(0x9560) + let var3 := mulmod(a_0, a_1_next_1, r) + let var4 := mulmod(var2, var3, r) + let var5 := addmod(var1, var4, r) + let var6 := 0x10000000000000000000000000000 + let a_2_next_1 := mload(0x9580) + let var7 := mulmod(a_0, a_2_next_1, r) + let var8 := mulmod(var6, var7, r) + let var9 := addmod(var5, var8, r) + let a_1 := mload(0x94c0) + let var10 := mulmod(a_1, a_0_next_1, r) + let var11 := mulmod(var2, var10, r) + let var12 := addmod(var9, var11, r) + let var13 := mulmod(a_1, a_1_next_1, r) + let var14 := mulmod(var6, var13, r) + let var15 := addmod(var12, var14, r) + let var16 := 0x3212e00cde6d2002b119d800000347fcb8 + let a_6_next_1 := mload(0x9720) + let var17 := mulmod(a_1, a_6_next_1, r) + let var18 := mulmod(var16, var17, r) + let var19 := addmod(var15, var18, r) + let a_2 := mload(0x94e0) + let var20 := mulmod(a_2, a_0_next_1, r) + let var21 := mulmod(var6, var20, r) + let var22 := addmod(var19, var21, r) + let a_5_next_1 := mload(0x9700) + let var23 := mulmod(a_2, a_5_next_1, r) + let var24 := mulmod(var16, var23, r) + let var25 := addmod(var22, var24, r) + let var26 := 0x297784894e27525bc342b7fde37dba9366 + let var27 := mulmod(a_2, a_6_next_1, r) + let var28 := mulmod(var26, var27, r) + let var29 := addmod(var25, var28, r) + let a_3 := mload(0x9500) + let a_4_next_1 := mload(0x96e0) + let var30 := mulmod(a_3, a_4_next_1, r) + let var31 := mulmod(var16, var30, r) + let var32 := addmod(var29, var31, r) + let var33 := mulmod(a_3, a_5_next_1, r) + let var34 := mulmod(var26, var33, r) + let var35 := addmod(var32, var34, r) + let var36 := 0x340f2ebe380a0f5eff4360543988a61dc2 + let var37 := mulmod(a_3, a_6_next_1, r) + let var38 := mulmod(var36, var37, r) + let var39 := addmod(var35, var38, r) + let a_4 := mload(0x9520) + let a_3_next_1 := mload(0x96c0) + let var40 := mulmod(a_4, a_3_next_1, r) + let var41 := mulmod(var16, var40, r) + let var42 := addmod(var39, var41, r) + let var43 := mulmod(a_4, a_4_next_1, r) + let var44 := mulmod(var26, var43, r) + let var45 := addmod(var42, var44, r) + let var46 := mulmod(a_4, a_5_next_1, r) + let var47 := mulmod(var36, var46, r) + let var48 := addmod(var45, var47, r) + let var49 := 0x13af65741744bd7bb2c6872df2b800320 + let var50 := mulmod(a_4, a_6_next_1, r) + let var51 := mulmod(var49, var50, r) + let var52 := addmod(var48, var51, r) + let a_5 := mload(0x95a0) + let var53 := mulmod(a_5, a_2_next_1, r) + let var54 := mulmod(var16, var53, r) + let var55 := addmod(var52, var54, r) + let var56 := mulmod(a_5, a_3_next_1, r) + let var57 := mulmod(var26, var56, r) + let var58 := addmod(var55, var57, r) + let var59 := mulmod(a_5, a_4_next_1, r) + let var60 := mulmod(var36, var59, r) + let var61 := addmod(var58, var60, r) + let var62 := mulmod(a_5, a_5_next_1, r) + let var63 := mulmod(var49, var62, r) + let var64 := addmod(var61, var63, r) + let var65 := 0x2cb9b546d20373eaf85e8f53db883cb548 + let var66 := mulmod(a_5, a_6_next_1, r) + let var67 := mulmod(var65, var66, r) + let var68 := addmod(var64, var67, r) + let a_6 := mload(0x95c0) + let var69 := mulmod(a_6, a_1_next_1, r) + let var70 := mulmod(var16, var69, r) + let var71 := addmod(var68, var70, r) + let var72 := mulmod(a_6, a_2_next_1, r) + let var73 := mulmod(var26, var72, r) + let var74 := addmod(var71, var73, r) + let var75 := mulmod(a_6, a_3_next_1, r) + let var76 := mulmod(var36, var75, r) + let var77 := addmod(var74, var76, r) + let var78 := mulmod(a_6, a_4_next_1, r) + let var79 := mulmod(var49, var78, r) + let var80 := addmod(var77, var79, r) + let var81 := mulmod(a_6, a_5_next_1, r) + let var82 := mulmod(var65, var81, r) + let var83 := addmod(var80, var82, r) + let var84 := 0xc8557e86f90d0d89eed6eb5349a0f8820 + let var85 := mulmod(a_6, a_6_next_1, r) + let var86 := mulmod(var84, var85, r) + let var87 := addmod(var83, var86, r) + let var88 := mulmod(var2, a_1, r) + let var89 := addmod(a_0, var88, r) + let var90 := mulmod(var6, a_2, r) + let var91 := addmod(var89, var90, r) + let var92 := addmod(var87, var91, r) + let var93 := mulmod(var2, a_1_next_1, r) + let var94 := addmod(a_0_next_1, var93, r) + let var95 := mulmod(var6, a_2_next_1, r) + let var96 := addmod(var94, var95, r) + let var97 := addmod(var92, var96, r) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) + let var98 := mulmod(var2, a_8, r) + let var99 := addmod(a_7, var98, r) + let a_9 := mload(0x9620) + let var100 := mulmod(var6, a_9, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(0, sub(r, var101), r) + let var103 := addmod(var97, var102, r) + let a_7_next_1 := mload(0x9740) + let var104 := 0x241eabfffeb153ffffb9feffffffffaaab + let var105 := mulmod(a_7_next_1, var104, r) + let var106 := addmod(0, sub(r, var105), r) + let var107 := addmod(var103, var106, r) + let var108 := addmod(0, sub(r, var16), r) + let var109 := addmod(var107, var108, r) + let a_8_next_1 := mload(0x9760) + let var110 := 0x73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb20000001 + let var111 := addmod(a_8_next_1, var110, r) + let var112 := 0x4000000000000000000000000000000000 + let var113 := mulmod(var111, var112, r) + let var114 := addmod(0, sub(r, var113), r) + let var115 := addmod(var109, var114, r) + let var116 := mulmod(var0, var115, r) + mstore(0xb8e0, var116) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 1 { + { + let var0 := 0x1 + let a_0 := mload(0x94a0) + let var1 := 0x10000000000000000000000000000 + let var2 := addmod(a_0, var1, r) + let var3 := 0x100000000000000 + let a_1 := mload(0x94c0) + let var4 := addmod(a_1, var1, r) + let var5 := mulmod(var3, var4, r) + let var6 := addmod(var2, var5, r) + let a_2 := mload(0x94e0) + let var7 := addmod(a_2, var1, r) + let var8 := mulmod(var1, var7, r) + let var9 := addmod(var6, var8, r) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) + let var10 := mulmod(var3, a_8, r) + let var11 := addmod(a_7, var10, r) + let a_9 := mload(0x9620) + let var12 := mulmod(var1, a_9, r) + let var13 := addmod(var11, var12, r) + let var14 := addmod(0, sub(r, var13), r) + let var15 := addmod(var9, var14, r) + let var16 := addmod(0, sub(r, var1), r) + let var17 := addmod(var15, var16, r) + let a_7_next_1 := mload(0x9740) + let var18 := 0x241eabfffeb153ffffb9feffffffffaaab + let var19 := mulmod(a_7_next_1, var18, r) + let var20 := addmod(0, sub(r, var19), r) + let var21 := addmod(var17, var20, r) + let var22 := 0xd9d44a30b019261257667fde3844a8cd6 + let var23 := addmod(0, sub(r, var22), r) + let var24 := addmod(var21, var23, r) + let a_8_next_1 := mload(0x9760) + let var25 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab00002 + let var26 := addmod(a_8_next_1, var25, r) + let var27 := 0x4000000000000000000000000000000000 + let var28 := mulmod(var26, var27, r) + let var29 := addmod(0, sub(r, var28), r) + let var30 := addmod(var24, var29, r) + let var31 := mulmod(var0, var30, r) + mstore(0xb8e0, var31) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x80) + let q_selector_acc := mload(q_selector_ptr) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 2 { + { + let var0 := 0x1 + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var1 := addmod(0, sub(r, a_0_next_1), r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace + let a_0 := mload(0x94a0) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x9500) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 + let a_1 := mload(0x94c0) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x9520) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_2 := mload(0x94e0) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xb8e0, var18) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x120) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + case 3 { + { + let var0 := 0x1 + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) + let var1 := addmod(0, sub(r, a_1_next_1), r) + let var2 := addmod(f_1, var1, r) + let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 + let a_0 := mload(0x94a0) + let var4 := mulmod(a_0, a_0, r) + let a_3 := mload(0x9500) + let var5 := mulmod(var4, a_3, r) + let var6 := mulmod(var3, var5, r) + let var7 := addmod(var2, var6, r) + let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 + let a_1 := mload(0x94c0) + let var9 := mulmod(a_1, a_1, r) + let a_4 := mload(0x9520) + let var10 := mulmod(var9, a_4, r) + let var11 := mulmod(var8, var10, r) + let var12 := addmod(var7, var11, r) + let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let a_2 := mload(0x94e0) + let var14 := mulmod(a_2, a_2, r) + let a_5 := mload(0x95a0) + let var15 := mulmod(var14, a_5, r) + let var16 := mulmod(var13, var15, r) + let var17 := addmod(var12, var16, r) + let var18 := mulmod(var0, var17, r) + mstore(0xb8e0, var18) + } + mstore(0xb280, mulmod(mload(0xb280), y, r)) + { + let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x120) + let q_selector_acc := mload(q_selector_ptr) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) + } + } + default { q_program_fail() } + } + // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. + case 0x0b { + // Operand layout packed into three bytes: + // high byte: selector bucket index; + // low u16 : y-power gap since this selector's + // previous contribution. + let q_selector_payload := shr(232, mload(q_pc)) + q_pc := add(q_pc, 3) + let q_sel_idx := shr(16, q_selector_payload) + let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 10)) { q_program_fail() } + if gt(q_sel_gap, 0x30) { q_program_fail() } + let q_eval := q_top + q_has_top := 0 + // Simple-selector identity: keep the same y-batch + // position as main identities, then advance only this + // selector bucket by its codegen-known gap. + // + // The global fully-evaluated accumulator is still + // multiplied by y so later main identities land at the + // same y powers as Rust's reverse fold. + mstore(0xb280, mulmod(mload(0xb280), y, r)) + let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) + let q_sel_acc := mload(q_target_ptr) + if q_sel_gap { + // Selector buckets are sparse in the global + // identity stream. Precomputed y^gap advances only + // this selector's local accumulator. + q_sel_acc := mulmod(q_sel_acc, mload(add(0xb2c0, shl(5, q_sel_gap))), r) + } + mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) + } + // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. + default { + q_program_fail() + } + } + // The VK-pinned bytecode must end exactly at q_end and every + // identity must have been consumed by a fold/native callback. + // This catches malformed generator output whose final opcode + // over-reads operands or leaves a partial expression live. + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0xb8e0)) { q_program_fail() } + + // Structured post-VM suffix. The current default uses this for + // regular trash constraints: it is smaller than fully unrolled + // Yul and cheaper than interpreting every trash operation. + // + // These generated blocks run after q_pc reaches q_end, but + // they still participate in the same identity order and write + // into the same numerator / selector accumulators. + { + let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) + { + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) + let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 + let var1 := mulmod(a_0_next_1, var0, r) + let var2 := addmod(f_0, var1, r) + let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 + let a_0 := mload(0x94a0) + let var4 := mulmod(var3, a_0, r) + let var5 := addmod(var2, var4, r) + let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f + let a_1 := mload(0x94c0) + let var7 := mulmod(var6, a_1, r) + let var8 := addmod(var5, var7, r) + let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 + let a_2 := mload(0x94e0) + let var10 := q_pow5(a_2) + let var11 := mulmod(var9, var10, r) + let var12 := addmod(var8, var11, r) + let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 + let a_3 := mload(0x9500) + let var14 := q_pow5(a_3) + let var15 := mulmod(var13, var14, r) + let var16 := addmod(var12, var15, r) + let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 + let a_4 := mload(0x9520) + let var18 := q_pow5(a_4) + let var19 := mulmod(var17, var18, r) + let var20 := addmod(var16, var19, r) + let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e + let a_5 := mload(0x95a0) + let var22 := q_pow5(a_5) + let var23 := mulmod(var21, var22, r) + let var24 := addmod(var20, var23, r) + let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 + let a_6 := mload(0x95c0) + let var26 := q_pow5(a_6) + let var27 := mulmod(var25, var26, r) + let var28 := addmod(var24, var27, r) + let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 + let a_7 := mload(0x95e0) + let var30 := q_pow5(a_7) + let var31 := mulmod(var29, var30, r) + let var32 := addmod(var28, var31, r) + let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) + let var34 := mulmod(a_1_next_1, var0, r) + let var35 := addmod(f_1, var34, r) + let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d + let var37 := mulmod(var36, a_0, r) + let var38 := addmod(var35, var37, r) + let var39 := 0x4d0ea7f9c3fda06d9535b0fdafd8338bd47c2200b284fa71a325ff41ac358028 + let var40 := mulmod(var39, a_1, r) + let var41 := addmod(var38, var40, r) + let var42 := 0x26cc223e16f47c20e17cc6069605fa5a8af05ea4f6eb36029a641d23b818eb10 + let var43 := mulmod(var42, var10, r) + let var44 := addmod(var41, var43, r) + let var45 := 0x31e823a45e567484c1544e310c0fa5cd66547a8f0dde659ac61698c30e838d25 + let var46 := mulmod(var45, var14, r) + let var47 := addmod(var44, var46, r) + let var48 := 0x275a20361ea91992193920270d3e2d1f6361880ac0a439c64bef815d4469ba85 + let var49 := mulmod(var48, var18, r) + let var50 := addmod(var47, var49, r) + let var51 := 0x5f3a15bab4ce4097b1edc3a25002694b92395ce355a8a12fe557459d9633f701 + let var52 := mulmod(var51, var22, r) + let var53 := addmod(var50, var52, r) + let var54 := 0x301cf56f9b4577112cc4241cddf6484aaadedbf1bbd0f2351adf2e41c2fb2ecd + let var55 := mulmod(var54, var26, r) + let var56 := addmod(var53, var55, r) + let var57 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f + let var58 := mulmod(var57, var30, r) + let var59 := addmod(var56, var58, r) + let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) + let f_2 := mload(0x9aa0) + let var61 := mulmod(a_3, var0, r) + let var62 := addmod(f_2, var61, r) + let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 + let var64 := mulmod(var63, a_0, r) + let var65 := addmod(var62, var64, r) + let var66 := 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a + let var67 := mulmod(var66, a_1, r) + let var68 := addmod(var65, var67, r) + let var69 := 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db + let var70 := mulmod(var69, var10, r) + let var71 := addmod(var68, var70, r) + let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) + let f_3 := mload(0x9ac0) + let var73 := mulmod(a_4, var0, r) + let var74 := addmod(f_3, var73, r) + let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 + let var76 := mulmod(var75, a_0, r) + let var77 := addmod(var74, var76, r) + let var78 := 0x26c2cc87f95726b28f33ca03409a460ec987cfe12adae32769e3565865d07191 + let var79 := mulmod(var78, a_1, r) + let var80 := addmod(var77, var79, r) + let var81 := 0x4382d0938a760120dd6cef8f3b90a0c38abae475e3d21e39365472b76d780272 + let var82 := mulmod(var81, var10, r) + let var83 := addmod(var80, var82, r) + let var84 := mulmod(var69, var14, r) + let var85 := addmod(var83, var84, r) + let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) + let f_4 := mload(0x99c0) + let var87 := mulmod(a_5, var0, r) + let var88 := addmod(f_4, var87, r) + let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b + let var90 := mulmod(var89, a_0, r) + let var91 := addmod(var88, var90, r) + let var92 := 0x24822e1af9aa2887c912c87eb0f20bd332330e7e55cd784de67cb407a9f05520 + let var93 := mulmod(var92, a_1, r) + let var94 := addmod(var91, var93, r) + let var95 := 0x4e5280109d8f96b8bfb543a6b1af25fb56a9db616af85a90eedc558e3eb1ea29 + let var96 := mulmod(var95, var10, r) + let var97 := addmod(var94, var96, r) + let var98 := mulmod(var81, var14, r) + let var99 := addmod(var97, var98, r) + let var100 := mulmod(var69, var18, r) + let var101 := addmod(var99, var100, r) + let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) + let f_5 := mload(0x99e0) + let var103 := mulmod(a_6, var0, r) + let var104 := addmod(f_5, var103, r) + let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 + let var106 := mulmod(var105, a_0, r) + let var107 := addmod(var104, var106, r) + let var108 := 0x23a6684b942d726a22e4d5b8d8ff83aeaa773f62600184efe5d033d7c7c6e827 + let var109 := mulmod(var108, a_1, r) + let var110 := addmod(var107, var109, r) + let var111 := 0x1981b4b33d6a9dab957b351d981d3323e65da39493af5bc01f7e8ffe17f98d4e + let var112 := mulmod(var111, var10, r) + let var113 := addmod(var110, var112, r) + let var114 := mulmod(var95, var14, r) + let var115 := addmod(var113, var114, r) + let var116 := mulmod(var81, var18, r) + let var117 := addmod(var115, var116, r) + let var118 := mulmod(var69, var22, r) + let var119 := addmod(var117, var118, r) + let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) + let f_6 := mload(0x9a00) + let var121 := mulmod(a_7, var0, r) + let var122 := addmod(f_6, var121, r) + let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a + let var124 := mulmod(var123, a_0, r) + let var125 := addmod(var122, var124, r) + let var126 := 0x27e7119226c42a6d19c1541904b99ae40685511ed2e078964b74594d38340849 + let var127 := mulmod(var126, a_1, r) + let var128 := addmod(var125, var127, r) + let var129 := 0xd94c46a8456352aa44d7a885ab59e3a36664e6fb25e826f8a4cd79822f0533 + let var130 := mulmod(var129, var10, r) + let var131 := addmod(var128, var130, r) + let var132 := mulmod(var111, var14, r) + let var133 := addmod(var131, var132, r) + let var134 := mulmod(var95, var18, r) + let var135 := addmod(var133, var134, r) + let var136 := mulmod(var81, var22, r) + let var137 := addmod(var135, var136, r) + let var138 := mulmod(var69, var26, r) + let var139 := addmod(var137, var138, r) + let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) + let f_7 := mload(0x9a20) + let a_2_next_1 := mload(0x9580) + let var141 := mulmod(a_2_next_1, var0, r) + let var142 := addmod(f_7, var141, r) + let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 + let var144 := mulmod(var143, a_0, r) + let var145 := addmod(var142, var144, r) + let var146 := 0x40fa389feb2522bb934881ac9ed749aee2296502af592418c6b5675c0f560261 + let var147 := mulmod(var146, a_1, r) + let var148 := addmod(var145, var147, r) + let var149 := 0x1f61345b652161410c5e29f51e301ae56342af824bc110649393d2b911c50d3e + let var150 := mulmod(var149, var10, r) + let var151 := addmod(var148, var150, r) + let var152 := mulmod(var129, var14, r) + let var153 := addmod(var151, var152, r) + let var154 := mulmod(var111, var18, r) + let var155 := addmod(var153, var154, r) + let var156 := mulmod(var95, var22, r) + let var157 := addmod(var155, var156, r) + let var158 := mulmod(var81, var26, r) + let var159 := addmod(var157, var158, r) + let var160 := mulmod(var69, var30, r) + let var161 := addmod(var159, var160, r) + let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) + let f_26 := mload(0x9ba0) + let q_trash_one_minus_selector := addmod(1, sub(r, f_26), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0xa120), r) + let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_trash_eval, r)) + } + } + // Finish selector buckets by applying the codegen-known tail + // from each selector's last identity to the end of the global + // y-batch. + // + // After this step, every selector bucket is aligned with the + // final global y position and can be multiplied by its fixed + // selector commitment in the linearized MSM. + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0600)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x05e0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0580)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0520)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x04c0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0460)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0400)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x03a0)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0340)), r)) + } + { + let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0280)), r)) + } + + // Fully evaluated identities are the constant-polynomial side + // of the linearization query. Rust subtracts that grouped + // scalar into expected_eval, so Solidity stores -nu_y(x). + let linearization_expected_eval := addmod(0, sub(r, mload(0xb280)), r) + mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) + pop(y) + } + + // =============================================================== + // Prepare linearization scalars for the final PCS MSM. + // + // The linearized commitment is + // (1 - x^n) * Σ_i x_split^i * Q_i + // + Σ_j sel_acc_j * S_j_com, + // where x_split = x^(n-1). Instead of materializing that point + // with a standalone G1MSM here, PCS block 5 expands the + // linearized commitment into its quotient and selector + // pairs inside the already-fused final MSM. + // + // QUOTIENT_MPTR is no longer a G1 point in this path. Its first + // two words carry: + // word 0: x_split + // word 1: one_minus_x_n + // =============================================================== + { + let x := mload(X_MPTR) + let k := 20 + // Compute both x^n and x^(n-1) with the same squaring walk: + // x_pow_2i tracks x^(2^i), while x_pow_2i_minus1 tracks + // x^(2^i - 1). + let x_pow_2i := x + let x_pow_2i_minus1 := 1 + for { let idx := 0 } lt(idx, k) { idx := add(idx, 1) } { + x_pow_2i_minus1 := mulmod( + mulmod(x_pow_2i_minus1, x_pow_2i_minus1, r), + x, + r + ) + x_pow_2i := mulmod(x_pow_2i, x_pow_2i, r) + } + let x_split := x_pow_2i_minus1 + let one_minus_x_n := addmod(1, sub(r, x_pow_2i), r) + + // PCS block 5 interprets this 2-word payload as scalar + // metadata, not as a materialized G1 point. + mstore(QUOTIENT_MPTR, x_split) + mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) + } + + // =============================================================== + // PCS computation (multi-prepare emitter from Step 5). + // + // The Rust lowering stage has already expanded the KZG multi-open + // equation into a sequence of generated Yul sub-blocks. Those + // blocks populate: + // - F_EVAL_MPTR / V_MPTR scalar batching values; + // - FINAL_COM_MPTR for the fused commitment MSM; + // - PAIRING_LHS_MPTR and PAIRING_RHS_MPTR for the final pairing. + // =============================================================== + { + // Generated PCS sub-block 1. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // 4 distinct rotation(s) + let x := mload(X_MPTR) + let omega := mload(OMEGA_MPTR) + let omega_inv := mload(OMEGA_INV_MPTR) + let x_pow_of_omega := x + mstore(add(ROT_POINTS_MPTR, 0x40), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega, r) + mstore(add(ROT_POINTS_MPTR, 0x60), x_pow_of_omega) + x_pow_of_omega := x + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x20), x_pow_of_omega) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) + mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) + } + // Generated PCS sub-block 2. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // pre-compute 43 x1 power(s) + let x1 := mload(X1_MPTR) + mstore(X1_POWERS_MPTR, 1) + let acc := 1 + let p := X1_POWERS_MPTR + for { let i := 0 } lt(i, 0x2a) { i := add(i, 1) } { + p := add(p, 0x20) + acc := mulmod(acc, x1, r) + mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) + } + } + // Generated PCS sub-block 3. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[0]: 43 evaluation term(s), 42 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x9980) + mstore(0xb2a0, 0x9480) + mstore(0xb2c0, 0xa020) + mstore(0xb2e0, 0xa040) + mstore(0xb300, 0xa0a0) + mstore(0xb320, 0xa0c0) + mstore(0xb340, 0xa120) + mstore(0xb360, 0x99a0) + mstore(0xb380, 0x99c0) + mstore(0xb3a0, 0x99e0) + mstore(0xb3c0, 0x9a00) + mstore(0xb3e0, 0x9a20) + mstore(0xb400, 0x9a40) + mstore(0xb420, 0x9a60) + mstore(0xb440, 0x9a80) + mstore(0xb460, 0x9aa0) + mstore(0xb480, 0x9ac0) + mstore(0xb4a0, 0x9ae0) + mstore(0xb4c0, 0x9b00) + mstore(0xb4e0, 0x9b20) + mstore(0xb500, 0x9b40) + mstore(0xb520, 0x9b60) + mstore(0xb540, 0x9b80) + mstore(0xb560, 0x9ba0) + mstore(0xb580, 0x9bc0) + mstore(0xb5a0, 0x9be0) + mstore(0xb5c0, 0x9c00) + mstore(0xb5e0, 0x9c20) + mstore(0xb600, 0x9c40) + mstore(0xb620, 0x9c60) + mstore(0xb640, 0x9c80) + mstore(0xb660, 0x9ca0) + mstore(0xb680, 0x9cc0) + mstore(0xb6a0, 0x9ce0) + mstore(0xb6c0, 0x9d00) + mstore(0xb6e0, 0x9d20) + mstore(0xb700, 0x9d40) + mstore(0xb720, 0x9d60) + mstore(0xb740, 0x9d80) + mstore(0xb760, 0x9da0) + mstore(0xb780, 0x9dc0) + mstore(0xb7a0, 0x9de0) + mstore(0xb7c0, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x9980) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x20) + for { let i := 1 } lt(i, 0x2b) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x20) + } + mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) + } + // Generated PCS sub-block 4. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[1]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9660) + let q_eval_set_1 := mload(0x9920) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x9680), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9940), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x96a0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9960), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) + } + // Generated PCS sub-block 5. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[2]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9fe0) + let q_eval_set_1 := mload(0xa000) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa060), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa080), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa0e0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa100), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) + } + // Generated PCS sub-block 6. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[3]: 11 evaluation term(s), 11 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x94a0) + mstore(0xb2a0, 0x9540) + mstore(0xb2c0, 0x97c0) + mstore(0xb2e0, 0x94c0) + mstore(0xb300, 0x9560) + mstore(0xb320, 0x97e0) + mstore(0xb340, 0x94e0) + mstore(0xb360, 0x9580) + mstore(0xb380, 0x9800) + mstore(0xb3a0, 0x9500) + mstore(0xb3c0, 0x96c0) + mstore(0xb3e0, 0x9820) + mstore(0xb400, 0x9520) + mstore(0xb420, 0x96e0) + mstore(0xb440, 0x9840) + mstore(0xb460, 0x95a0) + mstore(0xb480, 0x9700) + mstore(0xb4a0, 0x9860) + mstore(0xb4c0, 0x95c0) + mstore(0xb4e0, 0x9720) + mstore(0xb500, 0x9880) + mstore(0xb520, 0x95e0) + mstore(0xb540, 0x9740) + mstore(0xb560, 0x98a0) + mstore(0xb580, 0x9600) + mstore(0xb5a0, 0x9760) + mstore(0xb5c0, 0x98c0) + mstore(0xb5e0, 0x9620) + mstore(0xb600, 0x9780) + mstore(0xb620, 0x98e0) + mstore(0xb640, 0x9640) + mstore(0xb660, 0x97a0) + mstore(0xb680, 0x9900) + let q_eval_set_0 := mload(0x94a0) + let q_eval_set_1 := mload(0x9540) + let q_eval_set_2 := mload(0x97c0) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x60) + for { let i := 1 } lt(i, 0xb) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0xa0), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) + } + // Generated PCS sub-block 7. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // q_eval_set[4]: 5 evaluation term(s), 5 commitment term(s) (rolled, m>=4) + // stage per-(commit, rotation) eval source addresses + mstore(0xb280, 0x9e00) + mstore(0xb2a0, 0x9e20) + mstore(0xb2c0, 0x9e40) + mstore(0xb2e0, 0x9e60) + mstore(0xb300, 0x9e80) + mstore(0xb320, 0x9ea0) + mstore(0xb340, 0x9ec0) + mstore(0xb360, 0x9ee0) + mstore(0xb380, 0x9f00) + mstore(0xb3a0, 0x9f20) + mstore(0xb3c0, 0x9f40) + mstore(0xb3e0, 0x9f60) + mstore(0xb400, 0x9f80) + mstore(0xb420, 0x9fa0) + mstore(0xb440, 0x9fc0) + let q_eval_set_0 := mload(0x9e00) + let q_eval_set_1 := mload(0x9e20) + let q_eval_set_2 := mload(0x9e40) + let pow_p := add(X1_POWERS_MPTR, 0x20) + let eval_p := add(0xb280, 0x60) + for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { + let pow := mload(pow_p) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(mload(add(eval_p, 0x20))), pow, r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(mload(add(eval_p, 0x40))), pow, r), r) + pow_p := add(pow_p, 0x20) + eval_p := add(eval_p, 0x60) + } + mstore(add(Q_EVAL_SET_MPTR, 0x100), q_eval_set_0) + mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) + mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) + } + // Generated PCS sub-block 8. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // f_eval via Horner over 5 reversed set(s) + let x2 := mload(X2_MPTR) + let x3 := mload(X3_MPTR) + let f_eval := 0 + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let rot_pt_0 := mload(add(ROT_POINTS_MPTR, 0x0)) + let rot_pt_1 := mload(add(ROT_POINTS_MPTR, 0x20)) + let rot_pt_2 := mload(add(ROT_POINTS_MPTR, 0x40)) + let rot_pt_3 := mload(add(ROT_POINTS_MPTR, 0x60)) + // --- set 4 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_0), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_0), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_0), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_0, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x100)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x120)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x140)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 3 (cardinality 3) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let dx_2 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_1), r), r) + let lbasis_2 := 1 + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + lbasis_2 := mulmod(lbasis_2, addmod(rot_pt_1, sub(r, rot_pt_3), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, dx_2, r) + let bp_3 := mulmod(bp_2, lbasis_0, r) + let bp_4 := mulmod(bp_3, lbasis_1, r) + let bp_5 := mulmod(bp_4, lbasis_2, r) + let bq := scalar_inv(bp_5) + let lbasis_inv_2 := mulmod(bq, bp_4, r) + bq := mulmod(bq, lbasis_2, r) + let lbasis_inv_1 := mulmod(bq, bp_3, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_2 := mulmod(bq, bp_1, r) + bq := mulmod(bq, dx_2, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + den_inv := mulmod(den_inv, dx_inv_2, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xa0)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xc0)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + let term_2 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0xe0)), dx_inv_2, r), lbasis_inv_2, r) + eval := addmod(eval, sub(r, term_2), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 2 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_3), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_3), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_3, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x60)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x80)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 1 (cardinality 2) --- + { + let dx_0 := addmod(x3, sub(r, rot_pt_2), r) + let dx_1 := addmod(x3, sub(r, rot_pt_1), r) + let lbasis_0 := 1 + lbasis_0 := mulmod(lbasis_0, addmod(rot_pt_2, sub(r, rot_pt_1), r), r) + let lbasis_1 := 1 + lbasis_1 := mulmod(lbasis_1, addmod(rot_pt_1, sub(r, rot_pt_2), r), r) + let bp_0 := dx_0 + let bp_1 := mulmod(bp_0, dx_1, r) + let bp_2 := mulmod(bp_1, lbasis_0, r) + let bp_3 := mulmod(bp_2, lbasis_1, r) + let bq := scalar_inv(bp_3) + let lbasis_inv_1 := mulmod(bq, bp_2, r) + bq := mulmod(bq, lbasis_1, r) + let lbasis_inv_0 := mulmod(bq, bp_1, r) + bq := mulmod(bq, lbasis_0, r) + let dx_inv_1 := mulmod(bq, bp_0, r) + bq := mulmod(bq, dx_1, r) + let dx_inv_0 := bq + let den_inv := dx_inv_0 + den_inv := mulmod(den_inv, dx_inv_1, r) + let eval := mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), den_inv, r) + let term_0 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x20)), dx_inv_0, r), lbasis_inv_0, r) + eval := addmod(eval, sub(r, term_0), r) + let term_1 := mulmod(mulmod(mload(add(Q_EVAL_SET_MPTR, 0x40)), dx_inv_1, r), lbasis_inv_1, r) + eval := addmod(eval, sub(r, term_1), r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + // --- set 0 (cardinality 1) --- + { + let dx0 := addmod(x3, sub(r, rot_pt_2), r) + let dx0_inv := scalar_inv(dx0) + let eval := mulmod(addmod(calldataload(add(Q_EVAL_CPTR, 0x0)), sub(r, mload(add(Q_EVAL_SET_MPTR, 0x0))), r), dx0_inv, r) + f_eval := addmod(mulmod(f_eval, x2, r), eval, r) + } + mstore(F_EVAL_MPTR, f_eval) + } + // Generated PCS sub-block 9. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // build final_com and v (KZG single-opening proof, fused MSM) + // final MSM input length from circuit/VK shape: 78 term(s) + let x4 := mload(X4_MPTR) + let lin_x_split := mload(QUOTIENT_MPTR) + let lin_one_minus_x_n := mload(add(QUOTIENT_MPTR, 0x20)) + let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) + let x4_pow_full := 1 + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_1 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_2 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_3 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_4 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_5 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + let v := calldataload(Q_EVAL_CPTR) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) + v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), x4_pow_4, r), r) + v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_5, r), r) + mcopy(0xb280, 0xa840, 0x80) + mstore(0xb300, 1) + mcopy(0xb320, 0xa8c0, 0x80) + mstore(0xb3a0, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0xb3c0, 0xacc0, 0x80) + mstore(0xb440, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0xb460, 0xa940, 0x80) + mstore(0xb4e0, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0xb500, 0xad40, 0x80) + mstore(0xb580, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0xb5a0, 0xaec0, 0x80) + mstore(0xb620, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0xb640, 0x6700, 0x80) + mstore(0xb6c0, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0xb6e0, 0x6480, 0x80) + mstore(0xb760, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0xb780, 0x6500, 0x80) + mstore(0xb800, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0xb820, 0x6580, 0x80) + mstore(0xb8a0, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0xb8c0, 0x6600, 0x80) + mstore(0xb940, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0xb960, 0x6680, 0x80) + mstore(0xb9e0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0xba00, 0x6280, 0x80) + mstore(0xba80, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0xbaa0, 0x6300, 0x80) + mstore(0xbb20, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0xbb40, 0x6380, 0x80) + mstore(0xbbc0, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0xbbe0, 0x6400, 0x80) + mstore(0xbc60, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0xbc80, 0x6780, 0x80) + mstore(0xbd00, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0xbd20, 0x6800, 0x80) + mstore(0xbda0, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0xbdc0, 0x6880, 0x80) + mstore(0xbe40, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0xbe60, 0x6900, 0x80) + mstore(0xbee0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0xbf00, 0x6b00, 0x80) + mstore(0xbf80, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0xbfa0, 0x6e80, 0x80) + mstore(0xc020, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0xc040, 0x6f80, 0x80) + mstore(0xc0c0, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0xc0e0, 0x7000, 0x80) + mstore(0xc160, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0xc180, 0x7080, 0x80) + mstore(0xc200, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0xc220, 0x7100, 0x80) + mstore(0xc2a0, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0xc2c0, 0x7180, 0x80) + mstore(0xc340, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0xc360, 0x7200, 0x80) + mstore(0xc3e0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0xc400, 0x7280, 0x80) + mstore(0xc480, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0xc4a0, 0x7300, 0x80) + mstore(0xc520, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0xc540, 0x7380, 0x80) + mstore(0xc5c0, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0xc5e0, 0x7400, 0x80) + mstore(0xc660, mload(add(X1_POWERS_MPTR, 0x400))) + mcopy(0xc680, 0x7480, 0x80) + mstore(0xc700, mload(add(X1_POWERS_MPTR, 0x420))) + mcopy(0xc720, 0x7500, 0x80) + mstore(0xc7a0, mload(add(X1_POWERS_MPTR, 0x440))) + mcopy(0xc7c0, 0x7580, 0x80) + mstore(0xc840, mload(add(X1_POWERS_MPTR, 0x460))) + mcopy(0xc860, 0x7600, 0x80) + mstore(0xc8e0, mload(add(X1_POWERS_MPTR, 0x480))) + mcopy(0xc900, 0x7680, 0x80) + mstore(0xc980, mload(add(X1_POWERS_MPTR, 0x4a0))) + mcopy(0xc9a0, 0x7700, 0x80) + mstore(0xca20, mload(add(X1_POWERS_MPTR, 0x4c0))) + mcopy(0xca40, 0x7780, 0x80) + mstore(0xcac0, mload(add(X1_POWERS_MPTR, 0x4e0))) + mcopy(0xcae0, 0x7800, 0x80) + mstore(0xcb60, mload(add(X1_POWERS_MPTR, 0x500))) + mcopy(0xcb80, 0x7880, 0x80) + mstore(0xcc00, mload(add(X1_POWERS_MPTR, 0x520))) + let lin_query_scalar_41 := mload(add(X1_POWERS_MPTR, 0x540)) + let lin_cur_scalar_41 := mulmod(lin_query_scalar_41, lin_one_minus_x_n, r) + mcopy(0xcc20, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0xcca0, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xccc0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0xcd40, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xcd60, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0xcde0, lin_cur_scalar_41) + lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) + mcopy(0xce00, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0xce80, lin_cur_scalar_41) + mcopy(0xcea0, 0x6980, 0x80) + mstore(0xcf20, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0xcf40, 0x6a00, 0x80) + mstore(0xcfc0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0xcfe0, 0x6a80, 0x80) + mstore(0xd060, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0xd080, 0x6b80, 0x80) + mstore(0xd100, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0xd120, 0x6c00, 0x80) + mstore(0xd1a0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0xd1c0, 0x6c80, 0x80) + mstore(0xd240, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0xd260, 0x6d00, 0x80) + mstore(0xd2e0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0xd300, 0x6d80, 0x80) + mstore(0xd380, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0xd3a0, 0x6e00, 0x80) + mstore(0xd420, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0xd440, 0x6f00, 0x80) + mstore(0xd4c0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0xd4e0, 0xa6c0, 0x80) + mstore(0xd560, x4_pow_1) + mcopy(0xd580, 0xa740, 0x80) + mstore(0xd600, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0xd620, 0xa7c0, 0x80) + mstore(0xd6a0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0xd6c0, 0xac40, 0x80) + mstore(0xd740, x4_pow_2) + mcopy(0xd760, 0xadc0, 0x80) + mstore(0xd7e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0xd800, 0xae40, 0x80) + mstore(0xd880, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0xd8a0, 0xa140, 0x80) + mstore(0xd920, x4_pow_3) + mcopy(0xd940, 0xa1c0, 0x80) + mstore(0xd9c0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xd9e0, 0xa240, 0x80) + mstore(0xda60, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_3, r)) + mcopy(0xda80, 0xa2c0, 0x80) + mstore(0xdb00, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_3, r)) + mcopy(0xdb20, 0xa340, 0x80) + mstore(0xdba0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_3, r)) + mcopy(0xdbc0, 0xa3c0, 0x80) + mstore(0xdc40, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_3, r)) + mcopy(0xdc60, 0xa440, 0x80) + mstore(0xdce0, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_3, r)) + mcopy(0xdd00, 0xa4c0, 0x80) + mstore(0xdd80, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_3, r)) + mcopy(0xdda0, 0xa540, 0x80) + mstore(0xde20, mulmod(mload(add(X1_POWERS_MPTR, 0x100)), x4_pow_3, r)) + mcopy(0xde40, 0xa5c0, 0x80) + mstore(0xdec0, mulmod(mload(add(X1_POWERS_MPTR, 0x120)), x4_pow_3, r)) + mcopy(0xdee0, 0xa640, 0x80) + mstore(0xdf60, mulmod(mload(add(X1_POWERS_MPTR, 0x140)), x4_pow_3, r)) + mcopy(0xdf80, 0xa9c0, 0x80) + mstore(0xe000, x4_pow_4) + mcopy(0xe020, 0xaa40, 0x80) + mstore(0xe0a0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_4, r)) + mcopy(0xe0c0, 0xaac0, 0x80) + mstore(0xe140, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_4, r)) + mcopy(0xe160, 0xab40, 0x80) + mstore(0xe1e0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_4, r)) + mcopy(0xe200, 0xabc0, 0x80) + mstore(0xe280, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_4, r)) + mcopy(0xe2a0, F_COM_MPTR, 0x80) + mstore(0xe320, x4_pow_5) + if success { + // exact EIP-2537 G1MSM cost for 78 pair(s) + success := staticcall(525096, 0x0c, 0xb280, 0x30c0, FINAL_COM_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mstore(V_MPTR, v) + } + // Generated PCS sub-block 10. These lines are + // emitted by the multi-prepare lowering pass and are kept + // grouped so gas checkpoints can attribute their cost. + { + // Scale z*pi - vG before the final pairing check + // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) + mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x1080, FINAL_COM_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + if success { + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) + } + } + + // Batch the prevalidated public IVC accumulator pairing equation + // into the final KZG pairing. + // + // We do not simply multiply the two pairing equations together: + // two bad equations could cancel. Instead, after all four G1 + // pairing inputs are fixed, derive a verifier-local randomizer + // alpha and check: + // + // e(kzg_rhs + alpha * acc_rhs, G2_BASE) + // * e(kzg_lhs + alpha * acc_lhs, NEG_S_G2_BASE) == 1 + // + // If either original equation is bad, this combined equation + // holds for at most one alpha in Fr. + { + let batch_ptr := 0x1000 + + // Domain || vk_digest || KZG rhs/lhs || accumulator rhs/lhs. + // vk_digest makes alpha's binding to the verifying key local + // instead of transitive-through-the-points (audit I-7). + mstore(batch_ptr, 0x70616972696e672d62617463682d6163632d6b7a670000000000000000) + mstore(add(batch_ptr, 0x20), mload(VK_DIGEST_MPTR)) + mcopy(add(batch_ptr, 0x40), PAIRING_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0xc0), PAIRING_LHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x0140), ACC_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x01c0), ACC_LHS_MPTR, 0x80) + // alpha is Fiat-Shamir over the fully materialized pairing + // inputs. Replace the negligible zero draw with one so the + // accumulator equation cannot be accidentally dropped. + let acc_pair_alpha := mod(keccak256(batch_ptr, 0x0240), r) + if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } + + // PAIRING_RHS_MPTR += alpha * ACC_RHS_MPTR. + // First compute alpha * ACC_RHS with a one-pair G1MSM, then + // add it into the KZG RHS point. + mcopy(batch_ptr, ACC_RHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_RHS_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + + // PAIRING_LHS_MPTR += alpha * ACC_LHS_MPTR. + // Mirror the same randomized batching on the KZG LHS point. + mcopy(batch_ptr, ACC_LHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x80), acc_pair_alpha) + if success { + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + mcopy(add(batch_ptr, 0x80), PAIRING_LHS_MPTR, 0x80) + if success { + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) + success := and(success, eq(returndatasize(), 0x80)) + } + } + + // The Yul `ec_pairing` helper checks + // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 + // i.e. e(arg0, [1]_2) = e(arg1, [s]_2). + // + // The KZG pairing identity is + // e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2), + // so arg0 must be (final_com - v*G + x3*pi) and arg1 must be + // pi. The PAIRING_*_MPTR slots store + // PAIRING_LHS_MPTR := pi + // PAIRING_RHS_MPTR := final_com - v*G + x3*pi + // -- the historical "LHS"/"RHS" naming follows the dual MSM + // accumulator (left = pi, right = combined) and *not* the + // pairing argument order. Pass them swapped to ec_pairing. + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + + + + // Success path is terminal. Invalid inputs have already reverted, + // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } + mstore(RETURN_MPTR, 1) + return(RETURN_MPTR, 0x20) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2VerifyingKey.sol b/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2VerifyingKey.sol new file mode 100644 index 000000000..3d0114fa2 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/moonlight-wrap/Halo2VerifyingKey.sol @@ -0,0 +1,696 @@ +// SPDX-License-Identifier: CC0-1.0 + +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; + +/// @title Halo2 BLS12-381 verifying-key payload. +/// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. +/// @dev Byte 0 is an unconditional INVALID opcode so direct calls cannot execute payload bytes as code. The linked verifier pins the full runtime by length/codehash and copies the payload starting at byte 1. +/// @dev The layout follows the verifier inputs derived from +/// `midfall/proofs/src/plonk/mod.rs::VerifyingKey` and the transcript +/// `vk.hash_into` behavior used by `midfall/proofs/src/plonk/verifier.rs`. +/// +/// Layout (in 32-byte words, big-endian). The header slots are generated from +/// Rust's `VkHeaderLayout`; the byte offsets are absolute from the start of the +/// VK payload, not from byte 0 of the runtime. Runtime byte 0 is the INVALID +/// prefix; the verifier loads the payload via +/// `extcodecopy(vk, VK_MPTR, 0x01, vk_payload_len)` and then references each +/// slot by `VK_MPTR + i`. +/// +/// word 0 : vk_digest (Fq, transcript_repr of the CS) +/// word 1 : num_instances +/// word 2 : k (log2 of the domain size) +/// word 3 : n_inv (1/n in Fr) +/// word 4 : omega (n-th primitive root of unity) +/// word 5 : omega_inv +/// word 6 : omega_inv_to_l (omega_inv ^ |rotation_last|) +/// word 7 : has_accumulator (0 or 1) +/// word 8 : acc_offset (instance index of the accumulator) +/// word 9 : num_acc_limbs +/// word 10 : num_acc_limb_bits +/// word 11..14 : G1_BASE (4 words, EIP-2537 padded) +/// word 15..22 : G2_BASE (8 words, EIP-2537 padded) +/// word 23..30 : NEG_S_G2_BASE (8 words, EIP-2537 padded) +/// word 31..30 + Q_PAYLOAD : quotient VM constants + packed bytecode +/// word 31 + Q_PAYLOAD .. : fixed_comms (4 words each) +/// word 31 + Q_PAYLOAD + 4*N_FIXED .. +/// : permutation_comms (4 words each) +/// +/// Notes: +/// - `extcodehash` of this contract is pinned by the linked verifier via +/// `EXPECTED_VK_CODEHASH`, so any byte tweak is detected at deploy time. +/// - The quotient identity interpreter's static program is stored in this +/// pinned VK runtime. The verifier reads it from memory after `extcodecopy`, +/// avoiding verifier-side PUSH32/mstore immediates while keeping the program +/// covered by `EXPECTED_VK_CODEHASH`. +/// - The midnight-proofs migration bakes the per-lookup chunk counts, trashcan +/// structure, and `num_simple_selectors` into the generated verifier code. +contract Halo2VerifyingKey { + /// @notice Deploy the verifying-key payload as this contract's runtime bytecode. + /// @dev The constructor writes an INVALID byte followed by generated words into memory and returns that prefixed runtime. + /// @dev The transient construction buffer starts at `0x80`, preserving Solidity's reserved memory words. + constructor() { + assembly { + // Runtime layout: + // byte 0 : INVALID, so the payload cannot be executed + // byte 1..end : generated VK payload copied by Halo2Verifier + // + // `runtime` includes the INVALID prefix; `payload` points to word + // zero of the verifier-key data described in the contract NatSpec. + let runtime := 0x80 + let payload := add(runtime, 0x01) + mstore8(runtime, 0xfe) + // Header, base-point, and quotient-program words generated from + // VkPayloadLayout. The inline names on each mstore identify the + // exact slot in the rendered source. + mstore(add(payload, 0x0000), 0x56c0824fcff237dd8dc7b15f527346d9e1647d191815acb142500b0293e84f66) // vk_digest + mstore(add(payload, 0x0020), 0x0000000000000000000000000000000000000000000000000000000000000013) // num_instances + mstore(add(payload, 0x0040), 0x0000000000000000000000000000000000000000000000000000000000000014) // k + mstore(add(payload, 0x0060), 0x73eda0144f284aae5b6554d46c21576b363d4ec725be2bff1a400fff00001001) // n_inv + mstore(add(payload, 0x0080), 0x03e1c54bcb947035a57a6e07cb98de4a2f69e02d265e09d9fece7e0e39898d4b) // omega + mstore(add(payload, 0x00a0), 0x6c39442eade0092768ac033fa6f608750624a1bb17dbc026ef97c3573a28fc8c) // omega_inv + mstore(add(payload, 0x00c0), 0x2a0ccbaa0613f093f2bb6e97859513f0b613d8587eaa92db9e5604b8d6b68d45) // omega_inv_to_l + mstore(add(payload, 0x00e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // has_accumulator + mstore(add(payload, 0x0100), 0x000000000000000000000000000000000000000000000000000000000000000b) // acc_offset + mstore(add(payload, 0x0120), 0x0000000000000000000000000000000000000000000000000000000000000007) // num_acc_limbs + mstore(add(payload, 0x0140), 0x0000000000000000000000000000000000000000000000000000000000000038) // num_acc_limb_bits + mstore(add(payload, 0x0160), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) // g1_x_hi + mstore(add(payload, 0x0180), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) // g1_x_lo + mstore(add(payload, 0x01a0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) // g1_y_hi + mstore(add(payload, 0x01c0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) // g1_y_lo + mstore(add(payload, 0x01e0), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) // g2_x_c0_hi + mstore(add(payload, 0x0200), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) // g2_x_c0_lo + mstore(add(payload, 0x0220), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) // g2_x_c1_hi + mstore(add(payload, 0x0240), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) // g2_x_c1_lo + mstore(add(payload, 0x0260), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) // g2_y_c0_hi + mstore(add(payload, 0x0280), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) // g2_y_c0_lo + mstore(add(payload, 0x02a0), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) // g2_y_c1_hi + mstore(add(payload, 0x02c0), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) // g2_y_c1_lo + mstore(add(payload, 0x02e0), 0x000000000000000000000000000000000632aaf712568f19c297802268a7ad9d) // neg_s_g2_x_c0_hi + mstore(add(payload, 0x0300), 0xceea6ef7ab6f75a7d26781c8e90c7432bc5e99dcc219ba64010f3052123983ab) // neg_s_g2_x_c0_lo + mstore(add(payload, 0x0320), 0x00000000000000000000000000000000191ff4920e077a2f8cb3969ba8f05bc2) // neg_s_g2_x_c1_hi + mstore(add(payload, 0x0340), 0xaa9da8c95d640b1e051be7cf344ee7f01996df2568bf0e7ccd9eb70978820045) // neg_s_g2_x_c1_lo + mstore(add(payload, 0x0360), 0x0000000000000000000000000000000005f434ebf45460a864ad5b17497c7903) // neg_s_g2_y_c0_hi + mstore(add(payload, 0x0380), 0x71820c70c83aa186029536d22dff54373251152c28bc43269f95281eba1b012e) // neg_s_g2_y_c0_lo + mstore(add(payload, 0x03a0), 0x0000000000000000000000000000000004d1c747141bcac15e77e3e1d3853254) // neg_s_g2_y_c1_hi + mstore(add(payload, 0x03c0), 0xc8687afdad35345a04f79d9c2759007f6640676eb44aee7011ce5ad80744bb23) // neg_s_g2_y_c1_lo + mstore(add(payload, 0x03e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // quotient_const + mstore(add(payload, 0x0400), 0x00bbe1fbe9ef1e2d62490b03a82bf9ef10f5e9b2323033669cf6c50481f63e05) // quotient_const + mstore(add(payload, 0x0420), 0x0000000000000000000000000000000000000000000000000100000000000000) // quotient_const + mstore(add(payload, 0x0440), 0x0000000000000000000000000000000000010000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0460), 0x0000000000000000000000000000000000000000000000000000000400000000) // quotient_const + mstore(add(payload, 0x0480), 0x0000000000000000000000000000000000000000040000000000000000000000) // quotient_const + mstore(add(payload, 0x04a0), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x04c0), 0x0000000000000000000000000000000000000000000000100000000000000000) // quotient_const + mstore(add(payload, 0x04e0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000) // quotient_const + mstore(add(payload, 0x0500), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefeffffff00000001) // quotient_const + mstore(add(payload, 0x0520), 0x73eda753299d7d483339d80809a1d80553bca402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0540), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffb00000001) // quotient_const + mstore(add(payload, 0x0560), 0x73eda753299d7d483339d80809a1d80553bda402fbfe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0580), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffefffff001) // quotient_const + mstore(add(payload, 0x05a0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5beeffffffff00000001) // quotient_const + mstore(add(payload, 0x05c0), 0x00000000000000000000000000000010ff0726c5de281020ad8016cf6f691213) // quotient_const + mstore(add(payload, 0x05e0), 0x0000000000000000000000000000002c64068790f917282347187665718b04c8) // quotient_const + mstore(add(payload, 0x0600), 0x00000000000000000000000000000027241bb5338dce8a77499428839473bf3a) // quotient_const + mstore(add(payload, 0x0620), 0x0000000000000000000000000000002b7c4a26a1c7ae6fc4b499d04e4a463c4b) // quotient_const + mstore(add(payload, 0x0640), 0x000000000000000000000000000000274bc40fcf526be95333a8c22c79465298) // quotient_const + mstore(add(payload, 0x0660), 0x0000000000000000000000000000002a5ee6db49930276e2939d1c43ac82f744) // quotient_const + mstore(add(payload, 0x0680), 0x73eda753299d7d483339d80809a1d7edd77e26c51c38afb5debf8afa00c15cc3) // quotient_const + mstore(add(payload, 0x06a0), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000002) // quotient_const + mstore(add(payload, 0x06c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe53ebc627fffef6280001) // quotient_const + mstore(add(payload, 0x06e0), 0x0000000000000000000001000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0700), 0x0000000100000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0720), 0x6bc66e553973f396854f5626172ba135587d41e37a68209402355093fdcaaf6c) // quotient_const + mstore(add(payload, 0x0740), 0x63f31e3f446953960c9d6964474300df43ab29179970f642a28e39d6c883c74b) // quotient_const + mstore(add(payload, 0x0760), 0x73eda753299d7d483339d70809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x0780), 0x73eda752299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x07a0), 0x082738fdf02989b1adea81e1f27636cffb40621f85963b6afdcaaf6b02355095) // quotient_const + mstore(add(payload, 0x07c0), 0x0ffa8913e53429b2269c6ea3c25ed72610127aeb668d65bc5d71c628377c38b6) // quotient_const + mstore(add(payload, 0x07e0), 0x01ec1c0519185dfe86132d479c76d0786e1e037d0b05ca47648a1c5d29492c9b) // quotient_const + mstore(add(payload, 0x0800), 0x1e179025ca2470882b34e63940ccbd7ad9090bf414d43b696e093b5a8782528f) // quotient_const + mstore(add(payload, 0x0820), 0x4298bfee9a84c8ef8e83702075cb1abeb576f146636342e3db9ea6b0a4adf29d) // quotient_const + mstore(add(payload, 0x0840), 0x3c83b078e9abed278d042acc8f3bd21e228716f04be96af8025da860e2d1bba9) // quotient_const + mstore(add(payload, 0x0860), 0x427868260f487d1ef07edaadf37f5dbe705bd1318290f2577ae756b009c24f11) // quotient_const + mstore(add(payload, 0x0880), 0x03020e6a35e595abd22838beeadc45cfcb0545d85ca0ab2c59d44c203fac84a7) // quotient_const + mstore(add(payload, 0x08a0), 0x000000000000000000000000000000000000000000000000d201000000010000) // quotient_const + mstore(add(payload, 0x08c0), 0x0000000100001b7c3f8d3fe3c5b448f1bdeb2ae34698b72d6ce966fc208c05ed) // quotient_const + mstore(add(payload, 0x08e0), 0x73eda753299d7d483339d80809a1d7fd4057a4c12f26d1c1778e3360a6820001) // quotient_const + mstore(add(payload, 0x0900), 0x057797fa7060856f215654ff11006fe0acf6a437e9477bf6f782dfac86f2cf75) // quotient_const + mstore(add(payload, 0x0920), 0x0000000000000000000000000000000000000000000000000000000000000002) // quotient_const + mstore(add(payload, 0x0940), 0x0000000000000000000000000000000000000000000000000200000000000000) // quotient_const + mstore(add(payload, 0x0960), 0x0000000000000000000000000000000000020000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0980), 0x73eda753299d7d483339d80809a1d7e13511a4044eaa5bff4600ffff00005556) // quotient_const + mstore(add(payload, 0x09a0), 0x73eda753299d7d483339d80809a1d7c553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x09c0), 0x73eda753299d7d483339d80809a1d7d340dd972492de594de627fffefcb80349) // quotient_const + mstore(add(payload, 0x09e0), 0x73eda753299d7d483339d80809a1d7dbdc391ab4d8ac003bbd48021b82456c9b) // quotient_const + mstore(add(payload, 0x0a00), 0x73eda753299d7d483339d80809a1d7d1448ee5caf5eefcffbc9fabc57759e23f) // quotient_const + mstore(add(payload, 0x0a20), 0x73eda753299d7d483339d80809a1d80418c74cc18bb28443d3978d1fd47ffce1) // quotient_const + mstore(add(payload, 0x0a40), 0x0000000000000000000000000000006425c019bcda40056233b00000068ff970) // quotient_const + mstore(add(payload, 0x0a60), 0x00000000000000000000000000000052ef09129c4ea4b786856ffbc6fb7526cc) // quotient_const + mstore(add(payload, 0x0a80), 0x73eda753299d7d483339d80809a1d7d89a085d30fc8a7106a170ac2377c34ab9) // quotient_const + mstore(add(payload, 0x0aa0), 0x73eda753299d7d483339d80809a1d7f8ce65bb936f2d836012914aca65f077e1) // quotient_const + mstore(add(payload, 0x0ac0), 0x000000000000000000000000000000681e5d7c70141ebdfe86c0a873114c3b84) // quotient_const + mstore(add(payload, 0x0ae0), 0x000000000000000000000000000000297784894e27525bc342b7fde37dba9366) // quotient_const + mstore(add(payload, 0x0b00), 0x0000000000000000000000000000000275ecae82e897af7658d0e5be57000640) // quotient_const + mstore(add(payload, 0x0b20), 0x000000000000000000000000000000013af65741744bd7bb2c6872df2b800320) // quotient_const + mstore(add(payload, 0x0b40), 0x00000000000000000000000000000059736a8da406e7d5f0bd1ea7b710796a90) // quotient_const + mstore(add(payload, 0x0b60), 0x0000000000000000000000000000000c8557e86f90d0d89eed6eb5349a0f8820) // quotient_const + mstore(add(payload, 0x0b80), 0x0453ae02a5f228d8f956b5eab4fc92bbeea5eb26b6ae4b42b4fdfcfdf026aa22) // quotient_const + mstore(add(payload, 0x0ba0), 0x0000000000000000000000000000000000000000000000000000000800000000) // quotient_const + mstore(add(payload, 0x0bc0), 0x0000000000000000000000000000000000000000080000000000000000000000) // quotient_const + mstore(add(payload, 0x0be0), 0x0000000000000000000000000000000000000000000000000000000000002000) // quotient_const + mstore(add(payload, 0x0c00), 0x0000000000000000000000000000000000000000000000200000000000000000) // quotient_const + mstore(add(payload, 0x0c20), 0x73eda753299d7d483339d80809a1d7f454b67d3d21d64bde527fe92f9096edee) // quotient_const + mstore(add(payload, 0x0c40), 0x73eda753299d7d483339d80809a1d7d8efb71c7206e733dbb8e789998e74fb39) // quotient_const + mstore(add(payload, 0x0c60), 0x73eda753299d7d483339d80809a1d7de2fa1eecf722fd187b66bd77b6b8c40c7) // quotient_const + mstore(add(payload, 0x0c80), 0x73eda753299d7d483339d80809a1d7d9d7737d61384fec3a4b662fb0b5b9c3b6) // quotient_const + mstore(add(payload, 0x0ca0), 0x73eda753299d7d483339d80809a1d7de07f99433ad9272abcc573dd286b9ad69) // quotient_const + mstore(add(payload, 0x0cc0), 0x73eda753299d7d483339d80809a1d7daf4d6c8b96cfbe51c6c62e3bb537d08bd) // quotient_const + mstore(add(payload, 0x0ce0), 0x00000000000000000000000000000021fe0e4d8bbc5020415b002d9eded22426) // quotient_const + mstore(add(payload, 0x0d00), 0x00000000000000000000000000000058c80d0f21f22e50468e30eccae3160990) // quotient_const + mstore(add(payload, 0x0d20), 0x0000000000000000000000000000004e48376a671b9d14ee9328510728e77e74) // quotient_const + mstore(add(payload, 0x0d40), 0x00000000000000000000000000000056f8944d438f5cdf896933a09c948c7896) // quotient_const + mstore(add(payload, 0x0d60), 0x0000000000000000000000000000004e97881f9ea4d7d2a667518458f28ca530) // quotient_const + mstore(add(payload, 0x0d80), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb9194fffd) // quotient_const + mstore(add(payload, 0x0da0), 0x0000000000000000000002000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0dc0), 0x0000000200000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x0de0), 0x639f3557494a69e4d764d44424b56a655d3cdfc3f4d1e529046aa128fb955ed7) // quotient_const + mstore(add(payload, 0x0e00), 0x53f8952b5f3529e3e600fac084e429b93398ae2c32e39086451c73ae91078e95) // quotient_const + mstore(add(payload, 0x0e20), 0x72018b4e10851f49ad26aac06d2b078ce59fa085f4f891b79b75e3a1d6b6d366) // quotient_const + mstore(add(payload, 0x0e40), 0x55d6172d5f790cc00804f1cec8d51a8a7ab4980eeb2a209591f6c4a4787dad72) // quotient_const + mstore(add(payload, 0x0e60), 0x3154e7648f18b458a4b667e793d6bd469e46b2bc9c9b191b2461594e5b520d64) // quotient_const + mstore(add(payload, 0x0e80), 0x3769f6da3ff19020a635ad3b7a6605e731368d12b414f106fda2579e1d2e4458) // quotient_const + mstore(add(payload, 0x0ea0), 0x31753f2d1a55002942bafd5a16227a46e361d2d17d6d69a78518a94ef63db0f0) // quotient_const + mstore(add(payload, 0x0ec0), 0x70eb98e8f3b7e79c61119f491ec5923588b85e2aa35db0d2a62bb3dec0537b5a) // quotient_const + mstore(add(payload, 0x0ee0), 0x03d8380a3230bbfd0c265a8f38eda0f0dc3c06fa160b948ec91438ba52925936) // quotient_const + mstore(add(payload, 0x0f00), 0x3c2f204b9448e1105669cc7281997af5b21217e829a876d2dc1276b50f04a51e) // quotient_const + mstore(add(payload, 0x0f20), 0x1143d88a0b6c1496e9cd0838e1f45d7817303e89c6c829c8b73d4d62495be539) // quotient_const + mstore(add(payload, 0x0f40), 0x0519b99ea9ba5d06e6ce7d9114d5cc36f15089dd97d479f104bb50c2c5a37751) // quotient_const + mstore(add(payload, 0x0f60), 0x110328f8f4f37cf5adc3dd53dd5ce3778cf9fe60052388aff5cead6113849e21) // quotient_const + mstore(add(payload, 0x0f80), 0x057797fa7060856f215655ff11006fee9a1697597c277945ddaadfac83aad2c0) // quotient_const + mstore(add(payload, 0x0fa0), 0x0000000000000000000000000000003212e00cde6d2002b119d800000347fcb8) // quotient_const + mstore(add(payload, 0x0fc0), 0x000000000000000000000000000000340f2ebe380a0f5eff4360543988a61dc2) // quotient_const + mstore(add(payload, 0x0fe0), 0x0000000000000000000000000000002cb9b546d20373eaf85e8f53db883cb548) // quotient_const + mstore(add(payload, 0x1000), 0x0453ae02a5f228d8f956b6eab50092aaff9ec460d8863b22077de62a80bd8812) // quotient_const + mstore(add(payload, 0x1020), 0x73eda753299d7d4833351088b4af7508df8b737010b26601ef73fcbb87bd0000) // quotient_const + mstore(add(payload, 0x1040), 0x0aef2ff4e0c10ade42aca9fe2200e00159ed486fd28ef7edef05bf590de59ef3) // quotient_const + mstore(add(payload, 0x1060), 0x0000000000000000000000000000000000000000000000000000000000000006) // quotient_const + mstore(add(payload, 0x1080), 0x0000000000000000000000000000000000000000000000000600000000000000) // quotient_const + mstore(add(payload, 0x10a0), 0x0000000000000000000000000000000000060000000000000000000000000000) // quotient_const + mstore(add(payload, 0x10c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffffff) // quotient_const + mstore(add(payload, 0x10e0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefdffffff00000001) // quotient_const + mstore(add(payload, 0x1100), 0x73eda753299d7d483339d80809a1d80553bba402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1120), 0x0000000000000000000000000000000000000000000000000000000000000003) // quotient_const + mstore(add(payload, 0x1140), 0x0000000000000000000000000000000000030000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1160), 0x0000000000000000000000000000012c71404d368ec010269b10000013afec50) // quotient_const + mstore(add(payload, 0x1180), 0x000000000000000000000000000000f8cd1b37d4ebee2693904ff354f25f7464) // quotient_const + mstore(add(payload, 0x11a0), 0x000000000000000000000000000001385b1875503c5c39fb9441f95933e4b28c) // quotient_const + mstore(add(payload, 0x11c0), 0x0000000000000000000000000000007c668d9bea75f71349c827f9aa792fba32) // quotient_const + mstore(add(payload, 0x11e0), 0x0000000000000000000000000000000761c60b88b9c70e630a72b13b050012c0) // quotient_const + mstore(add(payload, 0x1200), 0x73eda753299d7d483339d80809a1d7a12dfd8a4625be569ccc4ffffef9700691) // quotient_const + mstore(add(payload, 0x1220), 0x73eda753299d7d483339d80809a1d7b264b49166b159a4787a900438048ad935) // quotient_const + mstore(add(payload, 0x1240), 0x00000000000000000000000000000003b0e305c45ce387318539589d82800960) // quotient_const + mstore(add(payload, 0x1260), 0x0000000000000000000000000000010c5a3fa8ec14b781d2375bf725316c3fb0) // quotient_const + mstore(add(payload, 0x1280), 0x000000000000000000000000000000259007b94eb27289dcc84c1f9dce2e9860) // quotient_const + mstore(add(payload, 0x12a0), 0x73eda753299d7d483339d80809a1d79d35602792ebdf9e00793f578beeb3c47d) // quotient_const + mstore(add(payload, 0x12c0), 0x73eda753299d7d483339d80809a1d802ddd0f5801766ac88a72f1a40a8fff9c1) // quotient_const + mstore(add(payload, 0x12e0), 0x73eda753299d7d483339d80809a1d7abe053165ef916860e42e15847ef869571) // quotient_const + mstore(add(payload, 0x1300), 0x73eda753299d7d483339d80809a1d7ec490dd323de5caac125229595cbe0efc1) // quotient_const + mstore(add(payload, 0x1320), 0x08a75c054be451b1f2ad6bd569f92577dd4bd64d6d5c968569fbf9fbe04d544d) // quotient_const + mstore(add(payload, 0x1340), 0x0000000000000000000000000000000000000000000000000000001800000000) // quotient_const + mstore(add(payload, 0x1360), 0x0000000000000000000000000000000000000000180000000000000000000000) // quotient_const + mstore(add(payload, 0x1380), 0x0000000000000000000000000000000000000000000000000000000000006000) // quotient_const + mstore(add(payload, 0x13a0), 0x0000000000000000000000000000000000000000000000600000000000000000) // quotient_const + mstore(add(payload, 0x13c0), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffff700000001) // quotient_const + mstore(add(payload, 0x13e0), 0x73eda753299d7d483339d80809a1d80553bda402f7fe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1400), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffffe001) // quotient_const + mstore(add(payload, 0x1420), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bdeffffffff00000001) // quotient_const + mstore(add(payload, 0x1440), 0x73eda753299d7d483339d80809a1d7e355af567743ae3bbda4ffd260212ddbdb) // quotient_const + mstore(add(payload, 0x1460), 0x73eda753299d7d483339d80809a1d7ac8bb094e10dd00bb871cf13341ce9f671) // quotient_const + mstore(add(payload, 0x1480), 0x73eda753299d7d483339d80809a1d7b70b86399be46147106cd7aef7d718818d) // quotient_const + mstore(add(payload, 0x14a0), 0x73eda753299d7d483339d80809a1d7ae5b2956bf70a17c7596cc5f626b73876b) // quotient_const + mstore(add(payload, 0x14c0), 0x73eda753299d7d483339d80809a1d7b6bc3584645b26895898ae7ba60d735ad1) // quotient_const + mstore(add(payload, 0x14e0), 0x73eda753299d7d483339d80809a1d7b095efed6fd9f96e39d8c5c777a6fa1179) // quotient_const + mstore(add(payload, 0x1500), 0x00000000000000000000000000000065fa2ae8a334f060c4110088dc9c766c72) // quotient_const + mstore(add(payload, 0x1520), 0x00000000000000000000000000000000000000000c0000000000000000000000) // quotient_const + mstore(add(payload, 0x1540), 0x0000000000000000000000000000010a58272d65d68af0d3aa92c660a9421cb0) // quotient_const + mstore(add(payload, 0x1560), 0x0000000000000000000000000000000000000000000000300000000000000000) // quotient_const + mstore(add(payload, 0x1580), 0x000000000000000000000000000000ead8a63f3552d73ecbb978f3157ab67b5c) // quotient_const + mstore(add(payload, 0x15a0), 0x000000000000000000000000000000852c1396b2eb457869d549633054a10e58) // quotient_const + mstore(add(payload, 0x15c0), 0x00000000000000000000000000000104e9bce7caae169e9c3b9ae1d5bda569c2) // quotient_const + mstore(add(payload, 0x15e0), 0x0000000000000000000000000000008274de73e5570b4f4e1dcd70eaded2b4e1) // quotient_const + mstore(add(payload, 0x1600), 0x000000000000000000000000000000ebc6985edbee8777f335f48d0ad7a5ef90) // quotient_const + mstore(add(payload, 0x1620), 0x0000000000000000000000000000007f1cb491dcb90764a7bad754cb0588e5cc) // quotient_const + mstore(add(payload, 0x1640), 0x73eda753299d7d48333049095fbd120c6b5942dd2166802b5297f978232a0002) // quotient_const + mstore(add(payload, 0x1660), 0x0000000000000000000006000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1680), 0x0000000600000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x16a0), 0x4302515f88a4431e1fbaccbc5adc8f25703b5745de78f77d0d3fe37cf2c01c83) // quotient_const + mstore(add(payload, 0x16c0), 0x140e70dbca64831b4b8f40317b68cd20f34ec27e98adf994cf555b0db316abbd) // quotient_const + mstore(add(payload, 0x16e0), 0x73eda753299d7d483339d60809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1700), 0x73eda751299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001) // quotient_const + mstore(add(payload, 0x1720), 0x104e71fbe05313635bd503c3e4ec6d9ff680c43f0b2c76d5fb955ed6046aa12a) // quotient_const + mstore(add(payload, 0x1740), 0x1ff51227ca6853644d38dd4784bdae4c2024f5d6cd1acb78bae38c506ef8716c) // quotient_const + mstore(add(payload, 0x1760), 0x70156f48f76cc14b27137d78d0b4371477819d08e9f2c77036ebc744ad6da6cb) // quotient_const + mstore(add(payload, 0x1780), 0x37be870795549c37dcd00b9588085d0fa1ab8c1ad655e52c23ed8949f0fb5ae3) // quotient_const + mstore(add(payload, 0x17a0), 0x62a9cec91e3168b1496ccfcf27ad7a8d3c8d65793936323648c2b29cb6a41ac8) // quotient_const + mstore(add(payload, 0x17c0), 0x6ed3edb47fe320414c6b5a76f4cc0bce626d1a256829e20dfb44af3c3a5c88b0) // quotient_const + mstore(add(payload, 0x17e0), 0x62ea7e5a34aa00528575fab42c44f48dc6c3a5a2fadad34f0a31529dec7b61e0) // quotient_const + mstore(add(payload, 0x1800), 0x6de98a7ebdd251f08ee9668a33e94c65bdb3185246bd05a64c5767be80a6f6b3) // quotient_const + mstore(add(payload, 0x1820), 0x0b88a81e969233f724730fadaac8e2d294b414ee4222bdac5b3caa2ef7b70ba2) // quotient_const + mstore(add(payload, 0x1840), 0x0000000300000000000000000000000000000000000000000000000000000000) // quotient_const + mstore(add(payload, 0x1860), 0x409fb98f933d25e8d0038d4f7b2a98dbc278a3b57cfb0879943764202d0def59) // quotient_const + mstore(add(payload, 0x1880), 0x43fe0c177a010031bf648c1cc285529323863340cc562ac9e7aaad86598b55df) // quotient_const + mstore(add(payload, 0x18a0), 0x33cb899e22443dc4bd6718aaa5dd18684590bb9d54587d5a25b7e826dc13afab) // quotient_const + mstore(add(payload, 0x18c0), 0x5a46b0715e6d5198819eb2abc26638708b1b23dc3e7cb23c4a1bb20f9686f7ad) // quotient_const + mstore(add(payload, 0x18e0), 0x0f4d2cdbfd2f1714b46b78b33e8164a4d3f19d98c77d6dd30e31f24850ea65f3) // quotient_const + mstore(add(payload, 0x1900), 0x419d6a1793664a2e73d2a85da4119e5513d7a0cde3bde4e90718f923a87532fa) // quotient_const + mstore(add(payload, 0x1920), 0x33097aeadeda76e1094b97fb9816aa66a6edfb200f6a9a0fe16c08233a8dda63) // quotient_const + mstore(add(payload, 0x1940), 0x09062b3ea1b0c1037678aa3cc094d16f610fd18915e201850d7ce460bf058df5) // quotient_const + mstore(add(payload, 0x1960), 0x0456a29a706afacf2158850711006fe0acf6a437e9477bf6f782dfac86f2cf7b) // quotient_const + mstore(add(payload, 0x1980), 0x0397cc06bc030aab970dabe70cd498bbeea8daaea65607bb6a872125fec74a10) // quotient_const + mstore(add(payload, 0x19a0), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb91950003) // quotient_const + mstore(add(payload, 0x19c0), 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140) // quotient_const + mstore(add(payload, 0x19e0), 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a) // quotient_const + mstore(add(payload, 0x1a00), 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db) // quotient_const + mstore(add(payload, 0x1a20), 0x0594e0109a0005958008060d000b0200011b000021020103070002000094a002) // quotient_program + mstore(add(payload, 0x1a40), 0x94c00394e00495000595200095400295600395800695a00795c00895e0099600) // quotient_program + mstore(add(payload, 0x1a60), 0x0a96200b96400c96600d96800e96a00496c00596e006970007972094a0009540) // quotient_program + mstore(add(payload, 0x1a80), 0x0295600395800496c00596e006970007972094c00295400395600495800596c0) // quotient_program + mstore(add(payload, 0x1aa0), 0x0696e00797000f972094e00395400495600595800696c00796e00f9700109720) // quotient_program + mstore(add(payload, 0x1ac0), 0x95000495400595600695800796c00f96e0109700119720952005954006956007) // quotient_program + mstore(add(payload, 0x1ae0), 0x95800f96c01096e011970012972095a00695400795600f95801096c01196e012) // quotient_program + mstore(add(payload, 0x1b00), 0x970013972095c00795400f95601095801196c01296e013970014972015974016) // quotient_program + mstore(add(payload, 0x1b20), 0x97800b03000121021703070001000094a00294c00394e0189500199520009540) // quotient_program + mstore(add(payload, 0x1b40), 0x0295600395801a95a01b95c00895e00996000a96201c96401d96601e96801f96) // quotient_program + mstore(add(payload, 0x1b60), 0xa01896c01996e01a97001b972094a00095400295600395801896c01996e01a97) // quotient_program + mstore(add(payload, 0x1b80), 0x001b972094c00295400395601895801996c01a96e01b970020972094e0039540) // quotient_program + mstore(add(payload, 0x1ba0), 0x1895601995801a96c01b96e020970021972095001895401995601a95801b96c0) // quotient_program + mstore(add(payload, 0x1bc0), 0x2096e021970022972095201995401a95601b95802096c02196e0229700239720) // quotient_program + mstore(add(payload, 0x1be0), 0x95a01a95401b95602095802196c02296e023970024972095c01b954020956021) // quotient_program + mstore(add(payload, 0x1c00), 0x95802296c02396e02497002597202697400b0300011b00012102270100000900) // quotient_program + mstore(add(payload, 0x1c20), 0x0695a00795c00895e00996000a96200b96400c96600094a00294c00394e00495) // quotient_program + mstore(add(payload, 0x1c40), 0x000595200d96800e96a01597401697800b04000121022801000008001a95a01b) // quotient_program + mstore(add(payload, 0x1c60), 0x95c00895e00996000a96201c96401d96600094a00294c00394e0189500199520) // quotient_program + mstore(add(payload, 0x1c80), 0x1e96801f96a02697400b040001210397a0290000000b2b0894a00994c00a94e0) // quotient_program + mstore(add(payload, 0x1ca0), 0x2a95402b95602c95800895e00996000a96202d97402e97600894a095e00994a0) // quotient_program + mstore(add(payload, 0x1cc0), 0x96000a94a096200994c095e00a94c096002f94c096a00a94e095e02f94e09680) // quotient_program + mstore(add(payload, 0x1ce0), 0x3094e096a02f95009660309500968031950096a02f9520964030952096603195) // quotient_program + mstore(add(payload, 0x1d00), 0x20968032952096a000954095402b954095602c95409580039560956033956097) // quotient_program + mstore(add(payload, 0x1d20), 0x20339580970034958097202f95a096203095a096403195a096603295a0968035) // quotient_program + mstore(add(payload, 0x1d40), 0x95a096a02f95c096003095c096203195c096403295c096603595c096803695c0) // quotient_program + mstore(add(payload, 0x1d60), 0x96a03396c096e03496c097003796c097203896e096e03796e097003996e09720) // quotient_program + mstore(add(payload, 0x1d80), 0x3a970097003b970097203c972097200d000b050000210397a03d030800021508) // quotient_program + mstore(add(payload, 0x1da0), 0x94a00994c00a94e00b95000c95202a95402b95602c95800d95a00e95c00895e0) // quotient_program + mstore(add(payload, 0x1dc0), 0x0996000a96200b96400c96600d96800e96a03e96c03f96e040970041972094a0) // quotient_program + mstore(add(payload, 0x1de0), 0x0895e00996000a96200b96400c96600d96800e96a094c00995e00a96000b9620) // quotient_program + mstore(add(payload, 0x1e00), 0x0c96400d96600e96804296a094e00a95e00b96000c96200d96400e9660429680) // quotient_program + mstore(add(payload, 0x1e20), 0x4396a095000b95e00c96000d96200e96404296604396804496a095200c95e00d) // quotient_program + mstore(add(payload, 0x1e40), 0x96000e96204296404396604496804596a095400095402b95602c95803e96c03f) // quotient_program + mstore(add(payload, 0x1e60), 0x96e040970041972095a00d95e00e96004296204396404496604596804696a095) // quotient_program + mstore(add(payload, 0x1e80), 0xc00e95e04296004396204496404596604696804796a015974016978003956095) // quotient_program + mstore(add(payload, 0x1ea0), 0x603e956095803f956096c040956096e041956097004895609720059580958040) // quotient_program + mstore(add(payload, 0x1ec0), 0x958096c041958096e0489580970049958097200796c096c04896c096e04996c0) // quotient_program + mstore(add(payload, 0x1ee0), 0x97004a96c097201096e096e04a96e097004b96e0972012970097004c97009720) // quotient_program + mstore(add(payload, 0x1f00), 0x14972097200d000b050001210397a04d03080001150894a00994c00a94e01c95) // quotient_program + mstore(add(payload, 0x1f20), 0x001d95202a95402b95602c95801e95a01f95c00895e00996000a96201c96401d) // quotient_program + mstore(add(payload, 0x1f40), 0x96601e96801f96a04e96c04f96e050970051972094a00895e00996000a96201c) // quotient_program + mstore(add(payload, 0x1f60), 0x96401d96601e96801f96a094c00995e00a96001c96201d96401e96601f968052) // quotient_program + mstore(add(payload, 0x1f80), 0x96a094e00a95e01c96001d96201e96401f96605296805396a095001c95e01d96) // quotient_program + mstore(add(payload, 0x1fa0), 0x001e96201f96405296605396805496a095201d95e01e96001f96205296405396) // quotient_program + mstore(add(payload, 0x1fc0), 0x605496805596a095400095402b95602c95804e96c04f96e050970051972095a0) // quotient_program + mstore(add(payload, 0x1fe0), 0x1e95e01f96005296205396405496605596805696a095c01f95e0529600539620) // quotient_program + mstore(add(payload, 0x2000), 0x5496405596605696805796a026974003956095604e956095804f956096c05095) // quotient_program + mstore(add(payload, 0x2020), 0x6096e051956097005895609720199580958050958096c051958096e058958097) // quotient_program + mstore(add(payload, 0x2040), 0x0059958097201b96c096c05896c096e05996c097005a96c097202196e096e05a) // quotient_program + mstore(add(payload, 0x2060), 0x96e097005b96e0972023970097005c9700972025972097200d000b0500012103) // quotient_program + mstore(add(payload, 0x2080), 0x97a05d0000000c390894a00994c00a94e02d97402e97600097a00097c00297e0) // quotient_program + mstore(add(payload, 0x20a0), 0x0398000898a00998c00a98e000954097c002954097e0039540980008954098a0) // quotient_program + mstore(add(payload, 0x20c0), 0x09954098c00a954098e002956097c003956097e05e9560988009956098a00a95) // quotient_program + mstore(add(payload, 0x20e0), 0x6098c02f9560996003958097c05e9580986038958098800a958098a02f958099) // quotient_program + mstore(add(payload, 0x2100), 0x4030958099600095e097a002960097a003962097a05e96c098403896c098605f) // quotient_program + mstore(add(payload, 0x2120), 0x96c098802f96c099203096c099403196c099605e96e098203896e098405f96e0) // quotient_program + mstore(add(payload, 0x2140), 0x98603a96e098802f96e099003096e099203196e099403296e099605e97009800) // quotient_program + mstore(add(payload, 0x2160), 0x38970098205f970098403a9700986060970098802f970098e030970099003197) // quotient_program + mstore(add(payload, 0x2180), 0x009920329700994035970099605e972097e038972098005f972098203a972098) // quotient_program + mstore(add(payload, 0x21a0), 0x4060972098603c972098802f972098c030972098e03197209900329720992035) // quotient_program + mstore(add(payload, 0x21c0), 0x9720994036972099600d000b060000210397a061020f000a001697800097a000) // quotient_program + mstore(add(payload, 0x21e0), 0x97c00297e00398000498200598400698600798800898a00998c00a98e00b9900) // quotient_program + mstore(add(payload, 0x2200), 0x0c992097a00095e00296000396200496400596600696800796a097c000954002) // quotient_program + mstore(add(payload, 0x2220), 0x95600395800496c00596e006970007972097e00295400395600495800596c006) // quotient_program + mstore(add(payload, 0x2240), 0x96e00797000f972098000395400495600595800696c00796e00f970010972098) // quotient_program + mstore(add(payload, 0x2260), 0x200495400595600695800796c00f96e010970011972098400595400695600795) // quotient_program + mstore(add(payload, 0x2280), 0x800f96c01096e011970012972098600695400795600f95801096c01196e01297) // quotient_program + mstore(add(payload, 0x22a0), 0x0013972098800795400f95601095801196c01296e013970014972095400898a0) // quotient_program + mstore(add(payload, 0x22c0), 0x0998c00a98e00b99000c99200d99400e996095600998a00a98c00b98e00c9900) // quotient_program + mstore(add(payload, 0x22e0), 0x0d99200e994042996095800a98a00b98c00c98e00d99000e9920429940439960) // quotient_program + mstore(add(payload, 0x2300), 0x96c00b98a00c98c00d98e00e990042992043994044996096e00c98a00d98c00e) // quotient_program + mstore(add(payload, 0x2320), 0x98e042990043992044994045996097000d98a00e98c04298e043990044992045) // quotient_program + mstore(add(payload, 0x2340), 0x994046996097200e98a04298c04398e04499004599204699404799600894a009) // quotient_program + mstore(add(payload, 0x2360), 0x94c00a94e00b95000c95200d95a00e95c01597400d99400e99600d000b060001) // quotient_program + mstore(add(payload, 0x2380), 0x210397a062020f0009000097a00097c00297e00398001898201998401a98601b) // quotient_program + mstore(add(payload, 0x23a0), 0x98800898a00998c00a98e01c99001d99201e994097a00095e002960003962018) // quotient_program + mstore(add(payload, 0x23c0), 0x96401996601a96801b96a097c00095400295600395801896c01996e01a97001b) // quotient_program + mstore(add(payload, 0x23e0), 0x972097e00295400395601895801996c01a96e01b970020972098000395401895) // quotient_program + mstore(add(payload, 0x2400), 0x601995801a96c01b96e020970021972098201895401995601a95801b96c02096) // quotient_program + mstore(add(payload, 0x2420), 0xe021970022972098401995401a95601b95802096c02196e02297002397209860) // quotient_program + mstore(add(payload, 0x2440), 0x1a95401b95602095802196c02296e023970024972098801b9540209560219580) // quotient_program + mstore(add(payload, 0x2460), 0x2296c02396e024970025972095400898a00998c00a98e01c99001d99201e9940) // quotient_program + mstore(add(payload, 0x2480), 0x1f996095600998a00a98c01c98e01d99001e99201f994052996095800a98a01c) // quotient_program + mstore(add(payload, 0x24a0), 0x98c01d98e01e99001f992052994053996096c01c98a01d98c01e98e01f990052) // quotient_program + mstore(add(payload, 0x24c0), 0x992053994054996096e01d98a01e98c01f98e052990053992054994055996097) // quotient_program + mstore(add(payload, 0x24e0), 0x001e98a01f98c05298e053990054992055994056996097201f98a05298c05398) // quotient_program + mstore(add(payload, 0x2500), 0xe05499005599205699405799600894a00994c00a94e01c95001d95201e95a01f) // quotient_program + mstore(add(payload, 0x2520), 0x95c02697401f99600d000b060001210397a0630000000b2b6494a06594c06694) // quotient_program + mstore(add(payload, 0x2540), 0xe06795406895606995806795e06896006996202d97402e97606a94a094a06594) // quotient_program + mstore(add(payload, 0x2560), 0xa094c06694a094e06b94c094c06c94c095c06c94e095a06d94e095c06c950095) // quotient_program + mstore(add(payload, 0x2580), 0x206d950095a06e950095c06f952095206e952095a070952095c067954095e068) // quotient_program + mstore(add(payload, 0x25a0), 0x95409600699540962068956095e0699560960071956096a069958095e0719580) // quotient_program + mstore(add(payload, 0x25c0), 0x968072958096a07395a095a07495a095c07595c095c071960097207196209700) // quotient_program + mstore(add(payload, 0x25e0), 0x729620972071964096e07296409700769640972071966096c072966096e07696) // quotient_program + mstore(add(payload, 0x2600), 0x609700779660972072968096c076968096e0779680970078968097207696a096) // quotient_program + mstore(add(payload, 0x2620), 0xc07796a096e07896a097007996a097200d000b070000210397a07a0308000215) // quotient_program + mstore(add(payload, 0x2640), 0x6494a06594c06694e07b95007c95206795406895606995807d95a07e95c06795) // quotient_program + mstore(add(payload, 0x2660), 0xe06896006996207f96408096608196808296a07f96c08096e081970082972094) // quotient_program + mstore(add(payload, 0x2680), 0xa06a94a06594c06694e07b95007c95207d95a07e95c095406795e06896006996) // quotient_program + mstore(add(payload, 0x26a0), 0x207f96408096608196808296a095606895e06996007f96208096408196608296) // quotient_program + mstore(add(payload, 0x26c0), 0x808396a095806995e07f96008096208196408296608396808496a096c07f95e0) // quotient_program + mstore(add(payload, 0x26e0), 0x8096008196208296408396608496808596a096e08095e0819600829620839640) // quotient_program + mstore(add(payload, 0x2700), 0x8496608596808696a097008195e08296008396208496408596608696808796a0) // quotient_program + mstore(add(payload, 0x2720), 0x97208295e08396008496208596408696608796808896a01597401697806b94c0) // quotient_program + mstore(add(payload, 0x2740), 0x94c07b94c094e07c94c095007d94c095207e94c095a08994c095c08a94e094e0) // quotient_program + mstore(add(payload, 0x2760), 0x7d94e095007e94e095208994e095a08b94e095c08c9500950089950095208b95) // quotient_program + mstore(add(payload, 0x2780), 0x0095a08d950095c08e952095208d952095a08f952095c09095a095a09195a095) // quotient_program + mstore(add(payload, 0x27a0), 0xc09295c095c00d000b070001210397a09303080001156494a06594c06694e094) // quotient_program + mstore(add(payload, 0x27c0), 0x95009595206795406895606995809695a09795c06795e0689600699620989640) // quotient_program + mstore(add(payload, 0x27e0), 0x9996609a96809b96a09896c09996e09a97009b972094a06a94a06594c06694e0) // quotient_program + mstore(add(payload, 0x2800), 0x9495009595209695a09795c095406795e06896006996209896409996609a9680) // quotient_program + mstore(add(payload, 0x2820), 0x9b96a095606895e06996009896209996409a96609b96809c96a095806995e098) // quotient_program + mstore(add(payload, 0x2840), 0x96009996209a96409b96609c96809d96a096c09895e09996009a96209b96409c) // quotient_program + mstore(add(payload, 0x2860), 0x96609d96809e96a096e09995e09a96009b96209c96409d96609e96809f96a097) // quotient_program + mstore(add(payload, 0x2880), 0x009a95e09b96009c96209d96409e96609f9680a096a097209b95e09c96009d96) // quotient_program + mstore(add(payload, 0x28a0), 0x209e96409f9660a09680a196a02697406b94c094c09494c094e09594c0950096) // quotient_program + mstore(add(payload, 0x28c0), 0x94c095209794c095a0a294c095c0a394e094e09694e095009794e09520a294e0) // quotient_program + mstore(add(payload, 0x28e0), 0x95a0a494e095c0a595009500a295009520a4950095a0a6950095c0a795209520) // quotient_program + mstore(add(payload, 0x2900), 0xa6952095a0a8952095c0a995a095a0aa95a095c0ab95c095c00d000b07000121) // quotient_program + mstore(add(payload, 0x2920), 0x0397a0ac0000000e100094a00294c00394e06795406895606995800095e00296) // quotient_program + mstore(add(payload, 0x2940), 0x000396202d97402e97600097c00297e003980008954095406895409560699540) // quotient_program + mstore(add(payload, 0x2960), 0x95800a956095607195609720719580970072958097207196c096e07296c09700) // quotient_program + mstore(add(payload, 0x2980), 0x7696c097203096e096e07696e097007796e09720329700970078970097203697) // quotient_program + mstore(add(payload, 0x29a0), 0x2097200d000b080000210397a0ad04010002150094a00294c00394e004950005) // quotient_program + mstore(add(payload, 0x29c0), 0x95206795406895606995800695a00795c00095e0029600039620049640059660) // quotient_program + mstore(add(payload, 0x29e0), 0x0696800796a07f96c08096e08197008297200097c00297e00398000498200598) // quotient_program + mstore(add(payload, 0x2a00), 0x4006986007988095400895406895606995807f96c08096e08197008297201597) // quotient_program + mstore(add(payload, 0x2a20), 0x401697800a956095607f9560958080956096c081956096e08295609700839560) // quotient_program + mstore(add(payload, 0x2a40), 0x97200c9580958081958096c082958096e0839580970084958097200e96c096c0) // quotient_program + mstore(add(payload, 0x2a60), 0x8396c096e08496c097008596c097204396e096e08596e097008696e097204597) // quotient_program + mstore(add(payload, 0x2a80), 0x009700879700972047972097200d000b080001210397a0ae04010001150094a0) // quotient_program + mstore(add(payload, 0x2aa0), 0x0294c00394e01895001995206795406895606995801a95a01b95c00095e00296) // quotient_program + mstore(add(payload, 0x2ac0), 0x000396201896401996601a96801b96a09896c09996e09a97009b97200097c002) // quotient_program + mstore(add(payload, 0x2ae0), 0x97e00398001898201998401a98601b988095400895406895606995809896c099) // quotient_program + mstore(add(payload, 0x2b00), 0x96e09a97009b97202697400a95609560989560958099956096c09a956096e09b) // quotient_program + mstore(add(payload, 0x2b20), 0x956097009c956097201d958095809a958096c09b958096e09c958097009d9580) // quotient_program + mstore(add(payload, 0x2b40), 0x97201f96c096c09c96c096e09d96c097009e96c097205396e096e09e96e09700) // quotient_program + mstore(add(payload, 0x2b60), 0x9f96e097205597009700a09700972057972097200d000b0800010594a01194a0) // quotient_program + mstore(add(payload, 0x2b80), 0x1194a005950008060d000b0900000594c01194c01194c005952008060d000b09) // quotient_program + mstore(add(payload, 0x2ba0), 0x00010594e01194e01194e00595a008060d000b0900011b00021b000305958008) // quotient_program + mstore(add(payload, 0x2bc0), 0x109aa00594a01194a01195000daf060594c01194c01195200db0060594e01194) // quotient_program + mstore(add(payload, 0x2be0), 0xe01195a00db1060d000b090001191f0000000000000000000000000000000000) // quotient_program + // Fixed-column commitment 0, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c00), 0x0000000000000000000000000000000016e98a681dca730dcbe651aec5493976) // fixed_comms[0].x_hi + mstore(add(payload, 0x2c20), 0x11944be61932e7d19a63786871335939b0eefa7e32507b68b4ad6fd89a5c1028) // fixed_comms[0].x_lo + mstore(add(payload, 0x2c40), 0x0000000000000000000000000000000010e6ae8d3becb251e60db7d788be7029) // fixed_comms[0].y_hi + mstore(add(payload, 0x2c60), 0x8dbcdfdbb394da2ca9725e07485d6b630569a66ec867aa0d605573ae82d550df) // fixed_comms[0].y_lo + // Fixed-column commitment 1, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2c80), 0x000000000000000000000000000000000fb2ef3076aa1a8068bec7ef35167c1c) // fixed_comms[1].x_hi + mstore(add(payload, 0x2ca0), 0xf1f26a24abaa3b4dc8c6d218021c08429e4c175654b5996eb4de74d17a5c188d) // fixed_comms[1].x_lo + mstore(add(payload, 0x2cc0), 0x00000000000000000000000000000000000f94fa3dcc144ffe6a4ace1de5f956) // fixed_comms[1].y_hi + mstore(add(payload, 0x2ce0), 0x76ad02ffc32c0345767b466f064aec3f1101ecd9eaf91ba5d4c145f792d1285a) // fixed_comms[1].y_lo + // Fixed-column commitment 2, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d00), 0x0000000000000000000000000000000016909a26057dec4152b5bd207c37f9a8) // fixed_comms[2].x_hi + mstore(add(payload, 0x2d20), 0xc3af2c8cfa63dfd83e34704255392445fae3c47d749e435e0291232659a8caeb) // fixed_comms[2].x_lo + mstore(add(payload, 0x2d40), 0x0000000000000000000000000000000000b9edb176686ec391a3684928dc0842) // fixed_comms[2].y_hi + mstore(add(payload, 0x2d60), 0x80c60eca091b8a3925e31ef1032793f2ef4e31225ab560bf721f17525dd476c8) // fixed_comms[2].y_lo + // Fixed-column commitment 3, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2d80), 0x0000000000000000000000000000000014861e20b064f637d60cc250ff3b8804) // fixed_comms[3].x_hi + mstore(add(payload, 0x2da0), 0x9b9697b5245d031c784398bdf950e99d7968c86e92c4ec1cb3821a69686dfb09) // fixed_comms[3].x_lo + mstore(add(payload, 0x2dc0), 0x000000000000000000000000000000000c117b524f84a06b024a40003e1c3aab) // fixed_comms[3].y_hi + mstore(add(payload, 0x2de0), 0x25253a3cf5b53d2f77d1f9328a9c0b32bea4f591eb470dbb599b5fbdfa7df46b) // fixed_comms[3].y_lo + // Fixed-column commitment 4, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e00), 0x000000000000000000000000000000001420bb2d8c182dda6c982eb820e035f6) // fixed_comms[4].x_hi + mstore(add(payload, 0x2e20), 0xa4a5c6c69376dfda17ac5588d34a0b313800b8d18ecd818f3e4e2671589ec691) // fixed_comms[4].x_lo + mstore(add(payload, 0x2e40), 0x0000000000000000000000000000000000855eb6333071d5bb90a0351dbf958c) // fixed_comms[4].y_hi + mstore(add(payload, 0x2e60), 0x13e5648bcafb55776c78e46031e4620cfda02728e26bb97f067a9a0cdf72aa9f) // fixed_comms[4].y_lo + // Fixed-column commitment 5, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2e80), 0x000000000000000000000000000000000707698990b767903c9aaf028c98b46a) // fixed_comms[5].x_hi + mstore(add(payload, 0x2ea0), 0xc4925e4e30db42105aade1a5cd7dd9b3d2222bd7085071b5c2212709cfcc2d46) // fixed_comms[5].x_lo + mstore(add(payload, 0x2ec0), 0x000000000000000000000000000000000a88a7d4f1148bd3efffaedb5be62777) // fixed_comms[5].y_hi + mstore(add(payload, 0x2ee0), 0x27fad92f1005d28d12ef861b5dcf8b580a1322048d469da5f84b26fa7f8d2e4d) // fixed_comms[5].y_lo + // Fixed-column commitment 6, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f00), 0x000000000000000000000000000000001495975f8f8c0cb6c54e1163fe045732) // fixed_comms[6].x_hi + mstore(add(payload, 0x2f20), 0x2be3eb4af919cb0eb6126028ebeab81d075dff85aa522989166bf6e01880c1be) // fixed_comms[6].x_lo + mstore(add(payload, 0x2f40), 0x0000000000000000000000000000000018bdd4eef361f4a6d23a6511b48e20fb) // fixed_comms[6].y_hi + mstore(add(payload, 0x2f60), 0x4080898637660f3f3371adfb48585dd2ad89c29cc260d604ce9cd68a31ff427d) // fixed_comms[6].y_lo + // Fixed-column commitment 7, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x2f80), 0x00000000000000000000000000000000166c1cd47b07ac0aee553f0b4277cf37) // fixed_comms[7].x_hi + mstore(add(payload, 0x2fa0), 0xb4822422a23ea90486c60a5b3b9052dff113041d51718304e781a5a7bd93bdc2) // fixed_comms[7].x_lo + mstore(add(payload, 0x2fc0), 0x00000000000000000000000000000000016c91bfb3ce38ba23df779d69abeaa4) // fixed_comms[7].y_hi + mstore(add(payload, 0x2fe0), 0x8aaa142a59667ce4eabfaf36c5453af4aaec6e596ad923693a2a7d1483c2fd20) // fixed_comms[7].y_lo + // Fixed-column commitment 8, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3000), 0x000000000000000000000000000000001262dde7fc4aeca256572897f5d601ef) // fixed_comms[8].x_hi + mstore(add(payload, 0x3020), 0x5b8b5586b8593d43cbcd11d2fd80fe2a2214269e5479b5d976cf507d5262c273) // fixed_comms[8].x_lo + mstore(add(payload, 0x3040), 0x0000000000000000000000000000000012bd810c9b8eaaf899f161e2d8c26762) // fixed_comms[8].y_hi + mstore(add(payload, 0x3060), 0xc3f299919bfa2b2809a027cc9bcac1799ef2d48b11403ce47a5341f7307d2cd4) // fixed_comms[8].y_lo + // Fixed-column commitment 9, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3080), 0x00000000000000000000000000000000096d06362268ed80162468c5e664e2bb) // fixed_comms[9].x_hi + mstore(add(payload, 0x30a0), 0x1a1a3b60a6c419cb6edb470b646e9ed4e021686e93f82d0c3ee2448368fe81be) // fixed_comms[9].x_lo + mstore(add(payload, 0x30c0), 0x0000000000000000000000000000000015ceb98a36a3576ad6db1163a0a98d2a) // fixed_comms[9].y_hi + mstore(add(payload, 0x30e0), 0x0535a28ca330834bb07c6682d32b20cd8171f4c0d9f063d768b6e3185b6ae44e) // fixed_comms[9].y_lo + // Fixed-column commitment 10, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3100), 0x0000000000000000000000000000000019731a2f803950b978082f57fdfffb38) // fixed_comms[10].x_hi + mstore(add(payload, 0x3120), 0x0de3c6bef55492c30e409d77ac9acb523958dea2856b12cb4e87608584ab499a) // fixed_comms[10].x_lo + mstore(add(payload, 0x3140), 0x0000000000000000000000000000000005f057d8c91984630d36a0df6c2ebb07) // fixed_comms[10].y_hi + mstore(add(payload, 0x3160), 0x3881713eb294d8cdf7bb0374cb722b7bb5080101a54638216ab4886f0276a475) // fixed_comms[10].y_lo + // Fixed-column commitment 11, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3180), 0x0000000000000000000000000000000004d63c6ffcf22c5a45cbc67e299c2b3d) // fixed_comms[11].x_hi + mstore(add(payload, 0x31a0), 0xcfd98199c525785d127006aca0f82774876a94e120b9bde61b9227ee96dc6841) // fixed_comms[11].x_lo + mstore(add(payload, 0x31c0), 0x0000000000000000000000000000000002385e4a7b9ab571a73f07d9574152e5) // fixed_comms[11].y_hi + mstore(add(payload, 0x31e0), 0xa45de038325c587523413d98b5feb6d2071c293abbeab53bf941d76b264f3369) // fixed_comms[11].y_lo + // Fixed-column commitment 12, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3200), 0x000000000000000000000000000000000eea9ee53f693242e50678f2ac4a5053) // fixed_comms[12].x_hi + mstore(add(payload, 0x3220), 0x6bc4aa60deae574c46e3cf56a8d14af17d603f8c222728fb09300eee5c5b326b) // fixed_comms[12].x_lo + mstore(add(payload, 0x3240), 0x0000000000000000000000000000000014aa3a043bb1340472428a1756ad82bb) // fixed_comms[12].y_hi + mstore(add(payload, 0x3260), 0x49fa2fee5ff945799006e851969947efb6351c7642b2683deb31537ea6209643) // fixed_comms[12].y_lo + // Fixed-column commitment 13, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3280), 0x000000000000000000000000000000000b7028c202ee5b6a74d6ccfe5990dc98) // fixed_comms[13].x_hi + mstore(add(payload, 0x32a0), 0x4381d45aa22ccb019d848829dd72d16143e5af158899fcc009d5ca17c97c3414) // fixed_comms[13].x_lo + mstore(add(payload, 0x32c0), 0x0000000000000000000000000000000006c6ca7119058dbb54d5302f125f9cef) // fixed_comms[13].y_hi + mstore(add(payload, 0x32e0), 0xdc6b68957c7b970427a4b5a3bd0aa7ff5fa033687afdc2a4e150a408b0a5f4d2) // fixed_comms[13].y_lo + // Fixed-column commitment 14, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3300), 0x0000000000000000000000000000000008a540ee790774da565637ec30ced988) // fixed_comms[14].x_hi + mstore(add(payload, 0x3320), 0x563c90d314d01996465fa3f893d5fb010b617a4edcabc5c8f8141584792a0067) // fixed_comms[14].x_lo + mstore(add(payload, 0x3340), 0x00000000000000000000000000000000071e81991f7c8f62be7df37cd485cb5e) // fixed_comms[14].y_hi + mstore(add(payload, 0x3360), 0x1f941d942cc15f814350867bcdf1521ef6e7e76b6928827688b0d3d14a6cf1a3) // fixed_comms[14].y_lo + // Fixed-column commitment 15, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3380), 0x0000000000000000000000000000000005a4ed142d6c05e535a91ff8244ca5cb) // fixed_comms[15].x_hi + mstore(add(payload, 0x33a0), 0xdda66355e156c19ea759a1f5aecd1225e16cb3f18397a66b055a2918a2a74d54) // fixed_comms[15].x_lo + mstore(add(payload, 0x33c0), 0x000000000000000000000000000000000ce5eb9724134cce6feb1c0ff1b66ab3) // fixed_comms[15].y_hi + mstore(add(payload, 0x33e0), 0x5e79b2b4087200f1d9a76ee4a2b3b5424266da9d518653e9391e9a94196f1d7f) // fixed_comms[15].y_lo + // Fixed-column commitment 16, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3400), 0x0000000000000000000000000000000009b07c1df13d348c1ed7cb560fcdd682) // fixed_comms[16].x_hi + mstore(add(payload, 0x3420), 0x55221aadb75410fa4526857c95cea6bf3a0efa98e31898de56ea7ff30f7c514a) // fixed_comms[16].x_lo + mstore(add(payload, 0x3440), 0x0000000000000000000000000000000011b2d20f8b12527aba78064c809753e2) // fixed_comms[16].y_hi + mstore(add(payload, 0x3460), 0x21cebde9bb41aa67abae156e3a179655388e51211ba5614fb696a7f0cc212560) // fixed_comms[16].y_lo + // Fixed-column commitment 17, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3480), 0x0000000000000000000000000000000010ce280498c90ca7501427caf6bcd6d9) // fixed_comms[17].x_hi + mstore(add(payload, 0x34a0), 0xb4987380f8d9e8ef710b81034a460c8da32362568e1e9fd6b2cb5297805c98ad) // fixed_comms[17].x_lo + mstore(add(payload, 0x34c0), 0x000000000000000000000000000000000292967ae2e91be4d3555cddb84538dd) // fixed_comms[17].y_hi + mstore(add(payload, 0x34e0), 0x0edcb077ce0626f751bee2673ff6b24c182fd2ae99a0cd83881a81a3facfcb6e) // fixed_comms[17].y_lo + // Fixed-column commitment 18, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3500), 0x0000000000000000000000000000000017cf661d855e771f61cab52918bc03e5) // fixed_comms[18].x_hi + mstore(add(payload, 0x3520), 0x329761bebc6704041c0430c64bc2d589210a4250f57e9dec51a807d4d6926f31) // fixed_comms[18].x_lo + mstore(add(payload, 0x3540), 0x000000000000000000000000000000000f50cacb58bf9101f3d7926b2e26675d) // fixed_comms[18].y_hi + mstore(add(payload, 0x3560), 0x036c7cf34f335946dfc1d1a6e148de3da13cabfe8e7507f10baca11e07231413) // fixed_comms[18].y_lo + // Fixed-column commitment 19, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3580), 0x0000000000000000000000000000000016caf868eda0aeb753fd8ec3bee96cbe) // fixed_comms[19].x_hi + mstore(add(payload, 0x35a0), 0x3217eade0bae8e908b6cfe1f144518cbaec5bbbbc6e6641d82702b3dd5997985) // fixed_comms[19].x_lo + mstore(add(payload, 0x35c0), 0x000000000000000000000000000000000a35da0b540c40574d639e726b9c7e6d) // fixed_comms[19].y_hi + mstore(add(payload, 0x35e0), 0xb45e6cbefb614f14a1721bdbca2dfcb20aaeb64926032522ba3de23420dc1d87) // fixed_comms[19].y_lo + // Fixed-column commitment 20, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3600), 0x000000000000000000000000000000000b7f1691be78a38c579c2ac6bef18d54) // fixed_comms[20].x_hi + mstore(add(payload, 0x3620), 0xf2d63e634590931fda91cce7af80f7c589cc37a98870e127c93e059ff020df07) // fixed_comms[20].x_lo + mstore(add(payload, 0x3640), 0x0000000000000000000000000000000012582699af0164d4335252425d0251c4) // fixed_comms[20].y_hi + mstore(add(payload, 0x3660), 0xa1af4e259a3f65f0065b2b05127538a8434de1326c4920ee45da346bbbc7d293) // fixed_comms[20].y_lo + // Fixed-column commitment 21, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3680), 0x000000000000000000000000000000000e004abc0ac243147c480dca32186a34) // fixed_comms[21].x_hi + mstore(add(payload, 0x36a0), 0x78642aaafe922adde54e02ae93479fa0ae45b75ed0229c2f358a0473d17c09a5) // fixed_comms[21].x_lo + mstore(add(payload, 0x36c0), 0x0000000000000000000000000000000013e0dac8358fff676678a7b7d854aee3) // fixed_comms[21].y_hi + mstore(add(payload, 0x36e0), 0x3fda118cfb39d426f3418966ed06283adbd804fbe8cf51fd89c2f5c410919f48) // fixed_comms[21].y_lo + // Fixed-column commitment 22, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3700), 0x00000000000000000000000000000000168c0d6883d0d5b67d3a5944ee0616a3) // fixed_comms[22].x_hi + mstore(add(payload, 0x3720), 0xce7ac537685a26b678bab6b1b50115c32a5f90b6ca56cb8560a40964840893ce) // fixed_comms[22].x_lo + mstore(add(payload, 0x3740), 0x00000000000000000000000000000000197d467f656fc49a1c5119d7cd826cc2) // fixed_comms[22].y_hi + mstore(add(payload, 0x3760), 0xbe6345c67f108e7e44f8ebfcebea5366fdee85d69947c8f74f623165dc1029b5) // fixed_comms[22].y_lo + // Fixed-column commitment 23, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3780), 0x00000000000000000000000000000000133f78d1d5ad537d358f421d115bdb2f) // fixed_comms[23].x_hi + mstore(add(payload, 0x37a0), 0x44eff93e623017fb4a06006877bd8504b8b614c6c877d73de7cdc2c2e69a8998) // fixed_comms[23].x_lo + mstore(add(payload, 0x37c0), 0x00000000000000000000000000000000067b406b38e3895daddca2f64de3ee21) // fixed_comms[23].y_hi + mstore(add(payload, 0x37e0), 0x5c7961f1d2f1904f7e2aa60bc20559610c868810183505527e28eea6089d6bf8) // fixed_comms[23].y_lo + // Fixed-column commitment 24, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3800), 0x000000000000000000000000000000000bd679e559d6bf9f498928a3074a0709) // fixed_comms[24].x_hi + mstore(add(payload, 0x3820), 0x23c4cf1d5823b59316852fd6653fe003a4a2da9ba443e4640993524c79a6f56d) // fixed_comms[24].x_lo + mstore(add(payload, 0x3840), 0x000000000000000000000000000000000821077b092335b9f70fb79a424cca13) // fixed_comms[24].y_hi + mstore(add(payload, 0x3860), 0x4e823d96a4fdeaf4154ab7c5ef023844695a10e9bfdf314847ccaa40205f980a) // fixed_comms[24].y_lo + // Fixed-column commitment 25, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3880), 0x00000000000000000000000000000000041f7c88095a1bbf5759d88a7d109853) // fixed_comms[25].x_hi + mstore(add(payload, 0x38a0), 0x4b1661f3b1b9012866e17c7550a5b52861ac915beba38767c7aeb04c1df330ac) // fixed_comms[25].x_lo + mstore(add(payload, 0x38c0), 0x000000000000000000000000000000000f2e8cc95c27bf0235efa88fa70bb3eb) // fixed_comms[25].y_hi + mstore(add(payload, 0x38e0), 0x59a5ef2602a9b70c5af7b0db755f10a8e0ffc380b61a580c3300fda8d6077854) // fixed_comms[25].y_lo + // Fixed-column commitment 26, stored as one + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3900), 0x000000000000000000000000000000001983933b9a69c960c9eda1f5942af11a) // fixed_comms[26].x_hi + mstore(add(payload, 0x3920), 0x22de694e7c8b2a5f8d920b50d6c90aca30d149ace5ff9feebf84a136abdfa9f4) // fixed_comms[26].x_lo + mstore(add(payload, 0x3940), 0x00000000000000000000000000000000162f68d07437f5dd432392f6a244044d) // fixed_comms[26].y_hi + mstore(add(payload, 0x3960), 0x4605b4457384fb65577df046c667a744f851fbdb0e8751aaeeb334bf32fbd3e0) // fixed_comms[26].y_lo + // Permutation commitment 0, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3980), 0x0000000000000000000000000000000014669fe860d72f984d085b33bfffa399) // permutation_comms[0].x_hi + mstore(add(payload, 0x39a0), 0x12a9b0d0acb1ff8fd119eee9d0870eab9324293b88df67b9e1ebd180db262c72) // permutation_comms[0].x_lo + mstore(add(payload, 0x39c0), 0x000000000000000000000000000000000f3292f0f7b5707fe0874c56fdae2e88) // permutation_comms[0].y_hi + mstore(add(payload, 0x39e0), 0x2a2ba44cc1a14f736d255905838f8438fb8dbfd65ad84bcbca748a749c58955e) // permutation_comms[0].y_lo + // Permutation commitment 1, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a00), 0x000000000000000000000000000000000ee7e9d684b203f28dd5424f3a511695) // permutation_comms[1].x_hi + mstore(add(payload, 0x3a20), 0xff88a9d696976adca8c54262e0f553756ec45b8e103453f8c18a07c3abcef5fd) // permutation_comms[1].x_lo + mstore(add(payload, 0x3a40), 0x00000000000000000000000000000000177029152e262c258c987bfbeeee14c8) // permutation_comms[1].y_hi + mstore(add(payload, 0x3a60), 0xe945c114efada4abdeff8610bea0cf199ef3f375cf15f6b437de67f3379c5801) // permutation_comms[1].y_lo + // Permutation commitment 2, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3a80), 0x000000000000000000000000000000000e2ead1ec1acc411940e7bea5d0f2114) // permutation_comms[2].x_hi + mstore(add(payload, 0x3aa0), 0x6b39327e36ebab84013dd8f5c2b3f8b576a125db88ff28db45a79a28df414b66) // permutation_comms[2].x_lo + mstore(add(payload, 0x3ac0), 0x00000000000000000000000000000000199b4ea033c01e1d83627c054ba6f2dc) // permutation_comms[2].y_hi + mstore(add(payload, 0x3ae0), 0x2a61b1277d07469ad65c413a1970c9cf179bedfc4cf2f61c1ee8ca5e4a01ab90) // permutation_comms[2].y_lo + // Permutation commitment 3, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b00), 0x0000000000000000000000000000000011af8f4e99c419588e05414f664ae8f6) // permutation_comms[3].x_hi + mstore(add(payload, 0x3b20), 0xdded731799d69d8f5a72c124a5c6df33153e15767000ef54648a8dd23687140a) // permutation_comms[3].x_lo + mstore(add(payload, 0x3b40), 0x0000000000000000000000000000000017ba99d9323e45fe54ec6ac4daaad8d3) // permutation_comms[3].y_hi + mstore(add(payload, 0x3b60), 0x0c0dbc53d3f5f54c05667eb0e4f28da8973657bf62915fcdfc551698f56aa574) // permutation_comms[3].y_lo + // Permutation commitment 4, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3b80), 0x000000000000000000000000000000000ed68fa806cd3171cb061afebf65799d) // permutation_comms[4].x_hi + mstore(add(payload, 0x3ba0), 0xf9014492a08de2420f1234158d2f1d214b5354b7ad8cfc753d2712e6bb9b20a5) // permutation_comms[4].x_lo + mstore(add(payload, 0x3bc0), 0x0000000000000000000000000000000013f05c6e0393baeec023451082007353) // permutation_comms[4].y_hi + mstore(add(payload, 0x3be0), 0x55211ac5dd3c2267dcf706d18263fa7b7916e44ab1d5309430542f75717187c4) // permutation_comms[4].y_lo + // Permutation commitment 5, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c00), 0x000000000000000000000000000000000a925e4ad4c2d71f096de0e53baea720) // permutation_comms[5].x_hi + mstore(add(payload, 0x3c20), 0x7a90226e678437d20f2ec9ae630075a12ae88a186bcfba1bc0444e5bc40245e7) // permutation_comms[5].x_lo + mstore(add(payload, 0x3c40), 0x0000000000000000000000000000000006ff1ce2b5e6c2c3cedd7a8465c5ed3a) // permutation_comms[5].y_hi + mstore(add(payload, 0x3c60), 0x72e4496eead09ba2cd0ef56fdd94942ed738b42b71caea3d1bd78b0ae3ec3f96) // permutation_comms[5].y_lo + // Permutation commitment 6, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3c80), 0x000000000000000000000000000000000190af4fe845524352a9f624463ae7ca) // permutation_comms[6].x_hi + mstore(add(payload, 0x3ca0), 0x406fa14f4319031fe5a7630ab2bd66c107d6cb05fa65b98ee0c5f0234c29d189) // permutation_comms[6].x_lo + mstore(add(payload, 0x3cc0), 0x0000000000000000000000000000000013036a5f7dca90b530955bf735da2920) // permutation_comms[6].y_hi + mstore(add(payload, 0x3ce0), 0x29b8759dc69e53d6c830a3bee081165d4d59442dacb50c2a06aded3193e88387) // permutation_comms[6].y_lo + // Permutation commitment 7, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d00), 0x0000000000000000000000000000000012b6e22b800c1a2dfde554f3312f4b50) // permutation_comms[7].x_hi + mstore(add(payload, 0x3d20), 0x1b75400de7c8c476dcc9fd72dcef80d9384da4e4af8f928a07e4a9fa485da8bc) // permutation_comms[7].x_lo + mstore(add(payload, 0x3d40), 0x00000000000000000000000000000000053da19ebda674bbc95d3d10ad04cb96) // permutation_comms[7].y_hi + mstore(add(payload, 0x3d60), 0xa410aecead2c7059df1e12a1a56117a830f0008a91551cec88f1ad548a379dfe) // permutation_comms[7].y_lo + // Permutation commitment 8, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3d80), 0x0000000000000000000000000000000016038223f354e7d7c75614116c58b1c9) // permutation_comms[8].x_hi + mstore(add(payload, 0x3da0), 0x866a8a39d6b30ea560b263c68da7e78210b2f2a7f9b19564388f2f2370d7cd6a) // permutation_comms[8].x_lo + mstore(add(payload, 0x3dc0), 0x000000000000000000000000000000000bdb1d40f473070bfa087ff87bc9c86d) // permutation_comms[8].y_hi + mstore(add(payload, 0x3de0), 0x0d57398e71f45845ebcd0aa24a9ea829718dfce700af80fab46b51ad73fb58f6) // permutation_comms[8].y_lo + // Permutation commitment 9, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e00), 0x00000000000000000000000000000000085c579504429306bbf0ec28b07f91a6) // permutation_comms[9].x_hi + mstore(add(payload, 0x3e20), 0x7326fabf9ec866a45ef4d294f390a7c497c7b990a62b602f5999fcaee0e7e9bc) // permutation_comms[9].x_lo + mstore(add(payload, 0x3e40), 0x000000000000000000000000000000000b042dab28398e8c0670ee6f04cca93a) // permutation_comms[9].y_hi + mstore(add(payload, 0x3e60), 0xa41ecc7205c8216513d027b4c1213d0e452c16898d7b2c811027598cf2715dc3) // permutation_comms[9].y_lo + // Permutation commitment 10, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3e80), 0x00000000000000000000000000000000093b6ec41dd5c2455b9d2dca635c472d) // permutation_comms[10].x_hi + mstore(add(payload, 0x3ea0), 0x386496c219c45db9f1a5d96c179d1028694626c4a1845aef384efaaa8053284b) // permutation_comms[10].x_lo + mstore(add(payload, 0x3ec0), 0x000000000000000000000000000000000386d1128886a4a879e8a93aa84b6525) // permutation_comms[10].y_hi + mstore(add(payload, 0x3ee0), 0x1061026f4dae08952816961f9d6fc0b8153784a5961f831b68f1e2ddc567efb7) // permutation_comms[10].y_lo + // Permutation commitment 11, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f00), 0x0000000000000000000000000000000000b6ed6814a099c4e435e42fc8ee4250) // permutation_comms[11].x_hi + mstore(add(payload, 0x3f20), 0x0e16b88c78069cebe20e4d647c8bb44acd6b9c6712d53a3ebd014e54a1a75e89) // permutation_comms[11].x_lo + mstore(add(payload, 0x3f40), 0x0000000000000000000000000000000001ae3805b59b2f2d53a59e2c19001e97) // permutation_comms[11].y_hi + mstore(add(payload, 0x3f60), 0xaace57ac2ba8b58550a740e897ba6278e409291e5c87466a01edd7cbe3b99f88) // permutation_comms[11].y_lo + // Permutation commitment 12, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x3f80), 0x000000000000000000000000000000000ca3af5a65ec51141f694db29736dd57) // permutation_comms[12].x_hi + mstore(add(payload, 0x3fa0), 0xa1cdbfa79f45aa80a6cdda8de3d51c4b16c95361b412c6f47e132eae7b69f7f7) // permutation_comms[12].x_lo + mstore(add(payload, 0x3fc0), 0x0000000000000000000000000000000018dc16284e57fccd755058b259b913c7) // permutation_comms[12].y_hi + mstore(add(payload, 0x3fe0), 0x7a54da0d0e206b7906878fd3e994ad04c66f48e19cd982d5651f99b48b992064) // permutation_comms[12].y_lo + // Permutation commitment 13, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4000), 0x00000000000000000000000000000000197560c82bf47486bee750469d626ce4) // permutation_comms[13].x_hi + mstore(add(payload, 0x4020), 0x2d40ce7489ff61a64a0eb55e6672926a40f06348ab575f926fbe4d87b672fcd7) // permutation_comms[13].x_lo + mstore(add(payload, 0x4040), 0x0000000000000000000000000000000007b4f39a579a01d4f01f4ab9bc841a7d) // permutation_comms[13].y_hi + mstore(add(payload, 0x4060), 0xe70541d6b79945e9569fd59388739f5b4ce57794044d287d7b0b58e35198caab) // permutation_comms[13].y_lo + // Permutation commitment 14, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4080), 0x0000000000000000000000000000000004e1b539a3a123291eaa8c85be3395e7) // permutation_comms[14].x_hi + mstore(add(payload, 0x40a0), 0x9c6845178d995313daf42680692210e1ec79278ec8b6e2da1d6f289609100404) // permutation_comms[14].x_lo + mstore(add(payload, 0x40c0), 0x0000000000000000000000000000000010d56b62afe35890b4f98b946daa8936) // permutation_comms[14].y_hi + mstore(add(payload, 0x40e0), 0xd83e014595585e2892e21bfc5e86f84d404b435dc1a689539d74773aba55dad4) // permutation_comms[14].y_lo + // Permutation commitment 15, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4100), 0x000000000000000000000000000000000f227bcaef796cabf0a93289fb8321e9) // permutation_comms[15].x_hi + mstore(add(payload, 0x4120), 0x1d43880fa5fcebab73c77c6bb406cc5e9386a81774635b5a6144b75ef10cd994) // permutation_comms[15].x_lo + mstore(add(payload, 0x4140), 0x000000000000000000000000000000000a849d95d37e501cc6272f325f88d677) // permutation_comms[15].y_hi + mstore(add(payload, 0x4160), 0xb25be83ce57eb9c32d382613930bbc0ef7ec349d44ac74a9383c111efcb73783) // permutation_comms[15].y_lo + // Permutation commitment 16, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4180), 0x000000000000000000000000000000000b77ada25b279742ec75d2014583ad79) // permutation_comms[16].x_hi + mstore(add(payload, 0x41a0), 0x427bfa99ab8a941648e1a11084fec2a6fdb8192f498cf3e272a5e786597273a1) // permutation_comms[16].x_lo + mstore(add(payload, 0x41c0), 0x00000000000000000000000000000000056788c108fd40583888b8771a70d3f6) // permutation_comms[16].y_hi + mstore(add(payload, 0x41e0), 0x364290f978d1fc201c0a76561a4c1cdde7643f7d76448dad2586c3eac2e34104) // permutation_comms[16].y_lo + // Permutation commitment 17, also a 4-word + // EIP-2537 padded uncompressed G1 slot. + mstore(add(payload, 0x4200), 0x0000000000000000000000000000000002ecf71916494d46153fdd513ead1917) // permutation_comms[17].x_hi + mstore(add(payload, 0x4220), 0x06401b48839713727c920f4591fe09ac3e8f4140ef1cd9e39b0d0bbe0b5b87d4) // permutation_comms[17].x_lo + mstore(add(payload, 0x4240), 0x000000000000000000000000000000000e7bab8cc3c714f06826c8f3695dc63c) // permutation_comms[17].y_hi + mstore(add(payload, 0x4260), 0xe7e931ff0dbe009cbb84fef0b99124905d90cdd6f5356a7ee7673548f6f0692c) // permutation_comms[17].y_lo + + // Return exactly the INVALID prefix plus the generated payload. The + // linked verifier pins this byte length and the resulting codehash. + return(runtime, 0x4281) + } + } +} \ No newline at end of file diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md new file mode 100644 index 000000000..83be67ef5 --- /dev/null +++ b/proofs/solidity-verifier/fixtures/moonlight-wrap/README.md @@ -0,0 +1,80 @@ +# Moonlight Wrap point_pair Replay Fixture + +> **STALE — regeneration required before any deployment.** These artifacts were +> rendered before the MF-1 fix (`MODEXP_GAS` raised to the EIP-7883 bound and a +> constructor modexp known-answer probe added), so the committed `.sol` files +> here still carry the old 1360 bound and no modexp probe. They remain valid +> inputs for the *replay* tests, which exercise verification logic rather than +> the modexp bound, but they must be regenerated (and their provenance rows +> below updated) on a host with the pinned solc before they are used as a +> deployment source. Regeneration needs a Moonlight checkout on the branch named +> below plus the SRS asset, neither of which the environment that applied the +> MF-1 fix could reach. + +Pre-rendered artifacts for the `point_pair` accumulator arm of +`tests/ivc_accumulator_replay.rs`. The IVC fixture next door covers +`AccumulatorEncoding::new` (explicit lhs/rhs scalars); this one covers +`AccumulatorEncoding::point_pair`, whose +`expected_acc_has_carried_scalars = false` template arms were otherwise only +ever compiled, never executed against a real proof. + +This is a single-contract render, so there is no `Halo2QuotientEvaluator.sol`; +the replay deploys the verifier with the verifying key alone. + +## Provenance + +| Field | Value | +| --- | --- | +| Source commit | `f894f75` (**this repository**, solidity-verifier, at fixture-render time; not the Midfall dependency revision — see the provenance-identities table in `docs/reference/REPRODUCIBLE_BUILDS.md`) | +| Rendered by | Moonlight `wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs` | +| Moonlight revision | `origin/codex/wrap-bench-cherry-picks` (`1940ea9`), rendered from a scratch worktree with the local-path Cargo unification below | +| Accumulator | `AccumulatorEncoding::point_pair(offset=11, num_limbs=7, num_limb_bits=56)` | +| Public inputs | 19 (accumulator occupies the trailing 8 words) | +| Verified on-chain | yes, 1,338,272 gas under revm Prague (2026-08-13 render: exact precompile gas bounds, typed errors, VM operand clamps, BUILD_ID, alpha vk-binding) | +| Native/Solidity trace | 244 trace points matched | + +## Regenerating + +Needs a Moonlight checkout on `origin/codex/wrap-bench-cherry-picks` placed as +a **sibling of this repository** -- its `aggregation/Cargo.toml` refers to +`../../midfall/proofs/solidity-verifier` by relative path, so the bench renders +with local codegen. + +That branch pins the Midfall crates to a fixed git revision, which would link a +second copy of `midnight-proofs` alongside the path-dependency one. Add a patch +to the Moonlight workspace `Cargo.toml` to unify them (do not commit it): + +```toml +[patch."https://github.com/EYBlockchain/midfall.git"] +midnight-circuits = { path = "/path/to/midfall/circuits" } +midnight-curves = { path = "/path/to/midfall/curves" } +midnight-proofs = { path = "/path/to/midfall/proofs" } +midnight-zk-stdlib = { path = "/path/to/midfall/zk_stdlib" } +blake2b_halo2 = { path = "/path/to/midfall/third_party/blake2b_halo2" } +``` + +Then, from the Moonlight checkout (needs `midnight-srs-2p19`/`2p20` in +`SRS_DIR`; takes several minutes): + +```bash +MOONLIGHT_RUN_WRAP_SOLIDITY_BENCH=1 \ +MOONLIGHT_RUN_WRAP_SOLIDITY_TRACE=1 \ +MOONLIGHT_WRAP_SOLIDITY_DUMP_DIR=/path/to/midfall/proofs/solidity-verifier/target/moonlight-wrap-solidity-dump \ +SRS_DIR=/path/to/midfall/zk_stdlib/examples/assets \ + cargo test --release --lib \ + wrap_circuit_composes_two_fold_children_from_four_dummy_fold_proofs \ + -- --ignored --nocapture + +cp target/moonlight-wrap-solidity-dump/{Halo2Verifier.sol,Halo2VerifyingKey.sol,\ +calldata.bin} fixtures/moonlight-wrap/ +``` + +Then update the source commit above. + +## Staleness + +A snapshot of the codegen that produced it. The artifacts are self-consistent, +so the replay keeps passing after a codegen change -- it just stops exercising +current output. Tracked by the commit stamp rather than an assertion, because +detecting drift means re-rendering, which needs the SRS and the Moonlight +checkout again. diff --git a/proofs/solidity-verifier/fixtures/moonlight-wrap/calldata.bin b/proofs/solidity-verifier/fixtures/moonlight-wrap/calldata.bin new file mode 100644 index 000000000..08caa3a0e Binary files /dev/null and b/proofs/solidity-verifier/fixtures/moonlight-wrap/calldata.bin differ diff --git a/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs b/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs index 49c7d08a8..01b51130e 100644 --- a/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs +++ b/proofs/solidity-verifier/fuzz/fuzz_targets/proof_repack.rs @@ -12,7 +12,10 @@ use midnight_proofs::{ plonk::{ keygen_vk_with_k, Advice, Circuit, Column, ConstraintSystem, Constraints, Error, Selector, }, - poly::{kzg::params::ParamsKZG, kzg::KZGCommitmentScheme, Rotation}, + poly::{ + kzg::{params::ParamsKZG, KZGCommitmentScheme}, + Rotation, + }, }; use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng}; diff --git a/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs b/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs index 616943c14..7fc515812 100644 --- a/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs +++ b/proofs/solidity-verifier/fuzz/fuzz_targets/solidity_calldata.rs @@ -14,7 +14,10 @@ use midnight_proofs::{ create_proof, keygen_pk, keygen_vk_with_k, Advice, Circuit, Column, ConstraintSystem, Constraints, Error, Selector, }, - poly::{kzg::params::ParamsKZG, kzg::KZGCommitmentScheme, Rotation}, + poly::{ + kzg::{params::ParamsKZG, KZGCommitmentScheme}, + Rotation, + }, transcript::{CircuitTranscript, Transcript}, }; use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng}; diff --git a/proofs/solidity-verifier/scripts/generate_artifact_manifest.sh b/proofs/solidity-verifier/scripts/generate_artifact_manifest.sh new file mode 100755 index 000000000..6092ed7a9 --- /dev/null +++ b/proofs/solidity-verifier/scripts/generate_artifact_manifest.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Emit the REVIEW_PACKET.md section-4 artifact-manifest table for one rendered +# fixture dump (a directory under target/, e.g. target/poseidon-fixture-dump). +# +# Hashes generated sources and fixture binaries with SHA-256, and — when the +# pinned solc is available — compiles each contract with the recorded flag set +# to report the runtime bytecode length and keccak256. The point of the script +# is that manifest rows are produced by a maintained tool instead of being +# hand-filled (M-5 / I-3, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). +# +# Usage: +# scripts/generate_artifact_manifest.sh [--runs ] +# +# Environment: +# SOLC path to solc (default: resolved like the test harness: +# $SOLC, then .solc/solc, then solc on PATH) +# SOLC_OPTIMIZE_RUNS overrides --runs / the default of 200 +set -euo pipefail + +dump_dir="${1:?usage: $0 [--runs ]}" +shift || true +runs="${SOLC_OPTIMIZE_RUNS:-200}" +while [[ $# -gt 0 ]]; do + case "$1" in + --runs) runs="${2:?--runs needs a value}"; shift 2 ;; + *) echo "unknown argument: $1" >&2; exit 1 ;; + esac +done + +[[ -d "$dump_dir" ]] || { echo "not a directory: $dump_dir" >&2; exit 1; } + +sha256() { + shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1 || sha256sum "$1" | cut -d' ' -f1 +} + +solc_bin="${SOLC:-}" +if [[ -z "$solc_bin" ]]; then + if [[ -x ".solc/solc" ]]; then solc_bin=".solc/solc"; else solc_bin="$(command -v solc || true)"; fi +fi + +# Compile one contract with the recorded flag set and print +# " " for its largest emitted runtime +# (the file's main contract). Requires solc and python3. +runtime_info() { + local src="$1" + [[ -n "$solc_bin" ]] || { echo "n/a (no solc)"; return; } + local hex + hex="$("$solc_bin" --bin-runtime --optimize --optimize-runs "$runs" --via-ir \ + --evm-version cancun --no-cbor-metadata "$src" 2>/dev/null \ + | awk '/^[0-9a-f]+$/ { if (length($0) > length(best)) best = $0 } END { print best }')" + [[ -n "$hex" ]] || { echo "n/a (compile failed)"; return; } + python3 - "$hex" <<'PY' +import sys + +data = bytes.fromhex(sys.argv[1]) +# keccak256 (pre-NIST padding), matching EVM EXTCODEHASH and the recorded +# runtime hashes in REPRODUCIBLE_BUILDS.md. CPython's hashlib sha3_256 is +# NIST SHA-3, NOT keccak, so require pycryptodome and say so if missing. +try: + from Crypto.Hash import keccak +except ImportError: + sys.stdout.write(f"{len(data)} bytes, keccak256 unavailable (pip install pycryptodome)") + sys.exit(0) +digest = keccak.new(digest_bits=256, data=data).hexdigest() +sys.stdout.write(f"{len(data)} bytes, 0x{digest}") +PY +} + +row() { printf '| %s | %s |\n' "$1" "$2"; } + +commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)" +dirty="clean" +[[ -n "$(git status --porcelain 2>/dev/null)" ]] && dirty="dirty" + +echo "### Artifact manifest: $dump_dir" +echo +echo "| Item | Value |" +echo "| --- | --- |" +row "Repository commit" "\`$commit\` (this repository; working tree $dirty)" +row "Rust toolchain" "\`rust-toolchain.toml\`" +row "Solidity compiler" "\`$("$solc_bin" --version 2>/dev/null | grep -o 'Version: .*' || echo 'n/a')\`" +row "Solidity flags" "\`--bin --optimize --optimize-runs $runs --via-ir --evm-version cancun --no-cbor-metadata\`" +row "Cargo features" "fill from the render invocation (not recoverable from the dump)" + +for name in Halo2Verifier Halo2VerifyingKey Halo2QuotientEvaluator; do + src="$dump_dir/$name.sol" + if [[ -f "$src" ]]; then + row "Generated $name source hash" "\`$(sha256 "$src")\`" + if [[ "$name" == "Halo2VerifyingKey" ]]; then + # The VK's deployed runtime (INVALID || payload) is constructed by its + # constructor, so the static --bin-runtime output is a stub and hashing + # it would be misleading. The authoritative pin is the generated + # EXPECTED_VK_CODEHASH constant checked on-chain. + row "$name runtime length/hash" "deploy-time constructed; pinned by EXPECTED_VK_LENGTH / EXPECTED_VK_CODEHASH in the verifier" + else + row "$name runtime length/hash" "$(runtime_info "$src")" + fi + else + row "Generated $name source hash" "not rendered (single-contract or embedded profile)" + fi +done + +for f in proof.bin calldata.bin instances.be instance.le; do + if [[ -f "$dump_dir/$f" ]]; then + row "Fixture \`$f\` hash" "\`$(sha256 "$dump_dir/$f")\`" + fi +done diff --git a/proofs/solidity-verifier/scripts/install_pinned_solc.sh b/proofs/solidity-verifier/scripts/install_pinned_solc.sh index 750092dc5..abd493fb8 100755 --- a/proofs/solidity-verifier/scripts/install_pinned_solc.sh +++ b/proofs/solidity-verifier/scripts/install_pinned_solc.sh @@ -14,6 +14,9 @@ case "$(uname -s)-$(uname -m)" in binary="solc-macosx-amd64-v${PINNED_SOLC_VERSION}" ;; Darwin-arm64) + # Upstream publishes no macosx-arm64 binary for this solc version; Apple + # Silicon deliberately runs the x86_64 binary under Rosetta 2. Same + # binary, same hash, same emitted bytecode. platform="macosx-amd64" binary="solc-macosx-amd64-v${PINNED_SOLC_VERSION}" ;; @@ -26,10 +29,40 @@ esac mkdir -p "$INSTALL_DIR" solc_path="$INSTALL_DIR/solc" +# Content hash, not version string. `--version` output is trivially forged by a +# substituted binary, and this project's entire reproducibility claim rests on +# the compiler being exactly this one. These are the official sha256 values +# from https://binaries.soliditylang.org//list.json for +# v0.8.30+commit.73712a01 (recorded 2026-08-12); they are also recorded in +# docs/reference/REPRODUCIBLE_BUILDS.md and the review-packet manifest. +# (A plain case statement, not `declare -A`: stock macOS bash is 3.2, which +# has no associative arrays.) +case "$platform" in + linux-amd64) pinned_solc_sha256="f3e987dc6ecebd4bd350c48edcbc320b46cf9e3109bd3fc3d88f1acaf4c428f7" ;; + macosx-amd64) pinned_solc_sha256="738dcdc6afddeb505ee4e4ef24f1c1fdba2b8c924e614cbbf5801a5b062dd683" ;; + *) pinned_solc_sha256="" ;; +esac + if [[ ! -x "$solc_path" ]] || ! "$solc_path" --version | grep -q "Version: ${PINNED_SOLC_VERSION}"; then url="https://binaries.soliditylang.org/${platform}/${binary}" echo "[install-solc] downloading $url" curl -fsSL "$url" -o "$solc_path" + expected="$pinned_solc_sha256" + if [[ -z "$expected" || "$expected" == TODO-* ]]; then + echo "[install-solc] no pinned SHA-256 recorded for platform '$platform'." >&2 + echo "[install-solc] Fetch it from https://binaries.soliditylang.org/${platform}/list.json" >&2 + echo "[install-solc] and set pinned_solc_sha256 in this script." >&2 + rm -f "$solc_path" + exit 1 + fi + actual="$(shasum -a 256 "$solc_path" 2>/dev/null | cut -d' ' -f1 || sha256sum "$solc_path" | cut -d' ' -f1)" + if [[ "$actual" != "$expected" ]]; then + echo "[install-solc] SHA-256 mismatch for $url" >&2 + echo "[install-solc] expected $expected" >&2 + echo "[install-solc] actual $actual" >&2 + rm -f "$solc_path" + exit 1 + fi chmod +x "$solc_path" fi diff --git a/proofs/solidity-verifier/scripts/record_srs_provenance.sh b/proofs/solidity-verifier/scripts/record_srs_provenance.sh new file mode 100755 index 000000000..b50643dc0 --- /dev/null +++ b/proofs/solidity-verifier/scripts/record_srs_provenance.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Record the SRS assets a verifier build trusts (H-1, +# docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). +# +# NEG_S_G2_BASE — the element every soundness guarantee of the deployed +# verifier rests on — is derived from the SRS at build time. Build-time code +# (src/lowering/vk.rs) now proves the SRS is internally consistent (s_g2 +# matches the tau underlying g_lagrange), but internal consistency cannot +# prove WHICH ceremony the asset came from. That link is this record: the +# SHA-256 of the exact asset bytes, checked against the table below and +# recorded in docs/reference/REPRODUCIBLE_BUILDS.md next to the ceremony +# reference. +# +# Usage: +# scripts/record_srs_provenance.sh [srs-dir] +# +# srs-dir defaults to $SRS_DIR, then ./.srs, then +# ../../zk_stdlib/examples/assets relative to this script. +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +default_assets="$script_dir/../../../zk_stdlib/examples/assets" + +srs_dir="${1:-${SRS_DIR:-}}" +if [[ -z "$srs_dir" ]]; then + if [[ -d "./.srs" ]]; then srs_dir="./.srs"; else srs_dir="$default_assets"; fi +fi +[[ -d "$srs_dir" ]] || { echo "SRS directory not found: $srs_dir" >&2; exit 1; } + +sha256() { + shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1 || sha256sum "$1" | cut -d' ' -f1 +} + +echo "### SRS provenance ($(date -u +%Y-%m-%dT%H:%M:%SZ), dir: $srs_dir)" +echo +echo "| Asset | Bytes | SHA-256 |" +echo "| --- | ---: | --- |" + +found=0 +for asset in midnight-srs-2p19 midnight-srs-2p20 midnight-srs-2p22 \ + bls_filecoin_2p19 bls_filecoin_2p13 bls_filecoin_2p12 bls_filecoin_2p6; do + path="$srs_dir/$asset" + [[ -f "$path" ]] || continue + found=1 + size="$(wc -c < "$path" | tr -d ' ')" + echo "| \`$asset\` | $size | \`$(sha256 "$path")\` |" +done + +if [[ "$found" == 0 ]]; then + echo >&2 + echo "no known SRS assets found in $srs_dir" >&2 + echo "download Midnight assets with: scripts/run_ivc_bench.sh (or curl from https://srs.midnight.network/)" >&2 + exit 1 +fi + +cat <<'EOF' + +Record these rows in docs/reference/REPRODUCIBLE_BUILDS.md ("SRS Provenance") +and compare against the hashes already recorded there before any deployment +build. The tau-binding of each asset's s_g2 (the NEG_S_G2_BASE source) is +checked by the gated test: + + HALO2_SOLIDITY_RUN_EVM_TESTS=1 cargo test --release --features evm \ + midnight_srs_assets_bind_s_g2_to_lagrange_tau +EOF diff --git a/proofs/solidity-verifier/src/api.rs b/proofs/solidity-verifier/src/api.rs index bbe20325a..ef36609ba 100644 --- a/proofs/solidity-verifier/src/api.rs +++ b/proofs/solidity-verifier/src/api.rs @@ -352,6 +352,16 @@ pub struct RenderOptions { pub quotient: RenderQuotient, /// Trace/gas diagnostic knobs. pub diagnostics: RenderDiagnostics, + /// Optional 32-byte provenance tag folded into the emitted `BUILD_ID` + /// constant (P10/L-8) — typically a hash of the generator git commit and + /// build context, e.g. `keccak256("commit=,dirty=")`. + /// + /// `None` (the default) keeps `BUILD_ID` a pure function of the feature + /// profile, VK, and SRS, so repository fixtures stay byte-stable across + /// commits. Deployment builds SHOULD set it and publish the preimage in + /// the deployment record; see + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. + pub provenance: Option<[u8; 32]>, } /// Rendered Solidity artifacts. @@ -389,6 +399,13 @@ pub enum RepackError { /// Hex encoding of the failing 48-byte compressed G1. bytes_hex: String, }, + /// One proof scalar is not a canonical Fr element (`>= r`). + NonCanonicalScalar { + /// Byte offset of the failing scalar in the native proof. + offset: usize, + /// Big-endian hex encoding of the failing 32-byte scalar. + bytes_hex: String, + }, } impl fmt::Display for RepackError { @@ -410,6 +427,11 @@ impl fmt::Display for RepackError { "invalid compressed G1 at compressed[{offset}..{}]: bytes = 0x{bytes_hex}", offset + layout::G1_COMPRESSED_BYTES ), + Self::NonCanonicalScalar { offset, bytes_hex } => write!( + f, + "non-canonical Fr scalar at compressed[{offset}..{}]: bytes = 0x{bytes_hex}", + offset + layout::WORD_BYTES + ), } } } @@ -446,6 +468,25 @@ pub enum GeneratorError { num_instances: usize, reason: &'static str, }, + /// The accumulator's fixed-base scalar tail asks for more bases than this + /// verifying key can supply. + /// + /// The tail must be empty (fully collapsed accumulator), or cover `-G` plus + /// every permutation commitment plus at most one scalar per fixed + /// commitment. + AccumulatorFixedBaseTailMismatch { + /// Tail scalars implied by `num_instances` and the encoding. + fixed_scalar_count: usize, + /// Smallest non-empty tail this verifying key supports (`-G` plus every + /// permutation commitment). + min_fixed_scalar_count: usize, + /// Largest tail this verifying key supports. + max_fixed_scalar_count: usize, + /// Fixed commitments in the verifying key. + num_fixed_comms: usize, + /// Permutation commitments in the verifying key. + num_permutation_comms: usize, + }, /// Internal render/layout planning failed before Solidity was emitted. Planning { /// Planning stage. @@ -498,6 +539,20 @@ impl fmt::Display for GeneratorError { f, "unsupported accumulator encoding: offset={offset}, num_limbs={num_limbs}, num_limb_bits={num_limb_bits}, num_instances={num_instances}; {reason}" ), + Self::AccumulatorFixedBaseTailMismatch { + fixed_scalar_count, + min_fixed_scalar_count, + max_fixed_scalar_count, + num_fixed_comms, + num_permutation_comms, + } => write!( + f, + "accumulator fixed-base scalar tail of {fixed_scalar_count} is not supported by \ + this verifying key ({num_fixed_comms} fixed, {num_permutation_comms} permutation \ + commitments): expected 0 (fully collapsed) or \ + {min_fixed_scalar_count}..={max_fixed_scalar_count}; adjust num_instances or the \ + accumulator offset" + ), Self::Planning { stage, message } => { write!(f, "generator planning failed during {stage}: {message}") } diff --git a/proofs/solidity-verifier/src/builder/api.rs b/proofs/solidity-verifier/src/builder/api.rs index 497a59f81..47b178786 100644 --- a/proofs/solidity-verifier/src/builder/api.rs +++ b/proofs/solidity-verifier/src/builder/api.rs @@ -48,7 +48,39 @@ impl<'a> SolidityGenerator<'a> { )?; if let Some(acc_encoding) = config.accumulator { acc_encoding.validate_for_num_instances(config.num_instances)?; + // The fixed-base scalar tail is derived from the *instance* length, + // but the bases it multiplies come from this verifying key: `-G`, + // then `fixed_comm_mptr + i * G1_BYTES` per fixed base, then the + // permutation commitments. Two tail lengths break that mapping: + // + // * Too long: the generated fixed-base pointers run past the end of the + // fixed-commitment region, silently aliasing permutation commitments and + // then arbitrary VK payload words as G1 bases. + // * Shorter than `-G` plus the permutation commitments: the base count + // underflows in the artifact emitter. + // + // A tail inside the range is left alone -- it covers a prefix of + // the fixed commitments, which keeps every pointer in region. + let fixed_scalar_count = acc_encoding.fixed_scalar_count(config.num_instances)?; + let num_fixed_comms = vk.fixed_commitments().len(); + let num_permutation_comms = vk.permutation().commitments().len(); + let min_fixed_scalar_count = 1 + num_permutation_comms; + let max_fixed_scalar_count = min_fixed_scalar_count + num_fixed_comms; + if fixed_scalar_count != 0 + && !(min_fixed_scalar_count..=max_fixed_scalar_count).contains(&fixed_scalar_count) + { + return Err(GeneratorError::AccumulatorFixedBaseTailMismatch { + fixed_scalar_count, + min_fixed_scalar_count, + max_fixed_scalar_count, + num_fixed_comms, + num_permutation_comms, + }); + } } + // Non-committed instance evaluations are reconstructed once from the + // public-input polynomial at the current rotation. Reject rotated + // instance queries until that path is keyed by `(column, rotation)`. if let Some((column, rotation)) = vk .cs() .instance_queries() @@ -61,7 +93,17 @@ impl<'a> SolidityGenerator<'a> { }); } - let meta = ConstraintSystemMeta::new(vk.cs(), config.num_committed_instances); + // Fallible: `ProtocolPlan::validate` rejects a range of unsupported + // constraint-system shapes -- an advice column that is absorbed but + // never opened by a PCS query being the most reachable authoring + // mistake. Panicking here would break this constructor's contract of + // reporting such shapes as a typed error. + let meta = ConstraintSystemMeta::try_new(vk.cs(), config.num_committed_instances).map_err( + |message| GeneratorError::Planning { + stage: "constraint system", + message, + }, + )?; Ok(Self { params, diff --git a/proofs/solidity-verifier/src/builder/render.rs b/proofs/solidity-verifier/src/builder/render.rs index 7d3eee235..c60a23563 100644 --- a/proofs/solidity-verifier/src/builder/render.rs +++ b/proofs/solidity-verifier/src/builder/render.rs @@ -15,6 +15,7 @@ struct VerifierRenderPlan { gas_checkpoints: bool, external_quotient: bool, expected_quotient: Option<(usize, U256)>, + provenance: Option<[u8; 32]>, } impl<'a> SolidityGenerator<'a> { @@ -39,6 +40,7 @@ impl<'a> SolidityGenerator<'a> { gas_checkpoints: options.diagnostics.gas_checkpoints, external_quotient, expected_quotient, + provenance: options.provenance, }; let verifier = self.render_verifier_source_with_plan(&inputs, &plan, render_plan)?; @@ -109,6 +111,7 @@ impl<'a> SolidityGenerator<'a> { render_plan.gas_checkpoints, render_plan.external_quotient, render_plan.expected_quotient, + render_plan.provenance, ) .render(&mut verifier_output) .map_err(|_| GeneratorError::Render { diff --git a/proofs/solidity-verifier/src/evm.rs b/proofs/solidity-verifier/src/evm.rs index 5a3ae7fec..09689eb2d 100644 --- a/proofs/solidity-verifier/src/evm.rs +++ b/proofs/solidity-verifier/src/evm.rs @@ -231,6 +231,78 @@ pub(crate) mod test { } } + /// Read the free-memory-pointer initializer from a runtime bytecode prefix. + /// + /// solc opens every contract by storing the initial free-memory pointer to + /// slot `0x40`. With `memoryguard` active -- which the verifier's + /// `assembly ("memory-safe")` annotation enables -- that value is raised + /// above `0x80` to reserve via-IR stack-to-memory spill slots, so the + /// returned value is the top of the region solc has claimed for itself. + /// + /// Recognises the two prologue encodings solc emits: + /// `PUSH1 v` / `PUSH2 v` followed by `PUSH1 0x40 MSTORE`, allowing an + /// optional `DUP1` between them (used when the value is reused). + /// Returns `None` if the prefix does not match, so callers can distinguish + /// "no reservation found" from "reservation is 0x80". + #[must_use] + pub fn runtime_free_memory_pointer_init(runtime: &[u8]) -> Option { + // PUSH1 v | PUSH2 v_hi v_lo + let (value, mut i) = match *runtime.first()? { + 0x60 => (usize::from(*runtime.get(1)?), 2), + 0x61 => ( + (usize::from(*runtime.get(1)?) << 8) | usize::from(*runtime.get(2)?), + 3, + ), + _ => return None, + }; + // Optional DUP1 when solc reuses the value. + if runtime.get(i) == Some(&0x80) { + i += 1; + } + // PUSH1 0x40 MSTORE + if runtime.get(i) == Some(&0x60) + && runtime.get(i + 1) == Some(&0x40) + && runtime.get(i + 2) == Some(&0x52) + { + return Some(value); + } + None + } + + /// Compile `solidity` and return the deployed runtime bytecode. + /// + /// # Panics + /// Panics under the same conditions as [`compile_solidity`]. + pub fn compile_solidity_runtime(solidity: impl AsRef<[u8]>) -> Vec { + let solc = require_pinned_solc(); + let mut process = Command::new(&solc) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("--bin-runtime") + .arg("--optimize") + .arg("--optimize-runs") + .arg(DEFAULT_OPTIMIZE_RUNS.to_string()) + .arg("--via-ir") + .arg("--evm-version") + .arg("cancun") + .arg("--no-cbor-metadata") + .arg("-") + .spawn() + .unwrap_or_else(|err| panic!("Failed to spawn process with command '{solc}':\n{err}")); + process.stdin.take().unwrap().write_all(solidity.as_ref()).unwrap(); + let output = process.wait_with_output().unwrap(); + let stdout = str::from_utf8(&output.stdout).unwrap(); + let marker = "Binary of the runtime part:"; + let start = stdout.find(marker).unwrap_or_else(|| { + panic!( + "Runtime compilation fails:\n{}", + str::from_utf8(&output.stderr).unwrap() + ) + }) + marker.len(); + hex::decode(stdout[start..].trim()).expect("solc runtime output should be hex") + } + /// Extract creation bytecode from solc's text `--bin` output. fn find_binary(stdout: &str) -> Option> { let start = stdout.find("Binary:")? + 8; @@ -274,6 +346,19 @@ pub(crate) mod test { /// are routed to revm's bundled implementations. The runner keeps an /// `InMemoryDB` across calls so tests can deploy once and call many /// times. + /// + /// MF-1 coverage gap, deliberately not papered over: this harness cannot + /// exercise a chain whose modexp is priced by EIP-7883. The pinned + /// revm 19 exposes `SpecId::OSAKA`, but that variant is Prague+EOF in this + /// version -- its `0x05` handler is `berlin_run`, i.e. EIP-2565 pricing + /// with the `/ 3` divisor -- so switching the spec here would produce + /// green tests that prove nothing about the repricing that bricked the + /// pre-fix bound. Raising real coverage needs a revm bump to a version + /// whose Osaka handler implements EIP-7883; until then, MF-1 is guarded + /// by `modexp_gas_bound_covers_every_live_schedule` (the bound is derived + /// from both EIP texts) and by the constructor's modexp known-answer + /// probe, which forwards the rendered bound and so fails at deployment on + /// any chain that prices modexp above it. #[derive(Default)] pub struct Evm { db: InMemoryDB, diff --git a/proofs/solidity-verifier/src/lib.rs b/proofs/solidity-verifier/src/lib.rs index 011af0a58..6cf2f30cf 100644 --- a/proofs/solidity-verifier/src/lib.rs +++ b/proofs/solidity-verifier/src/lib.rs @@ -58,8 +58,9 @@ pub const OUTER_SINGLE_H_COMMITMENT_ENABLED: bool = cfg!(feature = "outer-single #[cfg(feature = "evm")] pub use evm::test::{ - compile_solidity, compile_solidity_with_runs, pinned_solc_available, revm, solc_version, - CallOutcome, Evm, ALLOW_UNPINNED_SOLC_ENV, DEFAULT_OPTIMIZE_RUNS, PINNED_SOLC_VERSION, + compile_solidity, compile_solidity_runtime, compile_solidity_with_runs, pinned_solc_available, + revm, runtime_free_memory_pointer_init, solc_version, CallOutcome, Evm, + ALLOW_UNPINNED_SOLC_ENV, DEFAULT_OPTIMIZE_RUNS, PINNED_SOLC_VERSION, }; /// Test-only helper that exposes the internal BLS12-381 G1 to EIP-2537 diff --git a/proofs/solidity-verifier/src/lowering/abi/proof.rs b/proofs/solidity-verifier/src/lowering/abi/proof.rs index 3e085d1cb..d841dffe1 100644 --- a/proofs/solidity-verifier/src/lowering/abi/proof.rs +++ b/proofs/solidity-verifier/src/lowering/abi/proof.rs @@ -307,21 +307,53 @@ pub(crate) struct TranscriptBufferLayout { impl TranscriptBufferLayout { /// Derive transcript-buffer bounds from proof calldata shape and instances. - pub(crate) fn from_proof_layout(proof: &ProofCalldataLayout, num_instances: usize) -> Self { + /// + /// `phase_challenge_counts[i]` is the number of Fiat-Shamir challenges the + /// native schedule squeezes for user phase `i` (i.e. + /// `num_user_challenges`). It selects which advice commitments share + /// the pre-first-squeeze run: a phase that owns no challenge does not + /// trigger a squeeze, so its advices accumulate into the same run as + /// the following phase's. An empty slice is treated as "no phase owns a + /// challenge", which sums every advice phase as a safe upper bound. + pub(crate) fn from_proof_layout( + proof: &ProofCalldataLayout, + num_instances: usize, + phase_challenge_counts: &[usize], + ) -> Self { let word_absorb = layout::transcript::WORD_ABSORB_BYTES; let g1_absorb = layout::transcript::G1_ABSORB_BYTES; let squeeze_cushion = layout::transcript::POST_SQUEEZE_CUSHION_WORDS * WORD_BYTES; - let phase_1_advices = - proof.advice_phases.first().map(|section| section.item_count).unwrap_or(0); + // Advices absorbed before the first challenge squeeze. The native + // verifier squeezes a challenge only for phases that own one, and the + // unconditional `theta` squeeze follows every user phase, so all advice + // phases up to and INCLUDING the first challenge-bearing phase are + // absorbed into a single streaming run before the first squeeze. + // Counting only the first phase under-sized the buffer for the valid + // "advice in an early phase, challenge in a later phase" shape + // (e.g. a SecondPhase RLC column) and let the G1 absorb loop overrun + // `VK_MPTR`; see `plan_allows_challenge_phase_beyond_advice_phases`. + let mut pre_squeeze_advices = 0usize; + for (phase, section) in proof.advice_phases.iter().enumerate() { + pre_squeeze_advices += section.item_count; + if phase_challenge_counts.get(phase).copied().unwrap_or(0) > 0 { + break; + } + } let initial_run_bytes = word_absorb + g1_absorb + word_absorb + num_instances * word_absorb - + phase_1_advices * g1_absorb + + pre_squeeze_advices * g1_absorb + squeeze_cushion; - let eval_run_bytes = proof.quotient_limbs.byte_len + // Absorb bytes, so the quotient limbs are counted at the transcript's + // G1 absorb width -- NOT `quotient_limbs.byte_len`, which is a + // calldata length. The two happen to be equal today, but they have + // diverged before (a compressed 48/49-byte transcript encoding against + // padded calldata), and that divergence is what overruns the keccak + // buffer into `VK_MPTR`. + let eval_run_bytes = proof.quotient_limbs.item_count * g1_absorb + proof.evals.byte_len + proof.q_evals.byte_len + squeeze_cushion; @@ -464,7 +496,7 @@ mod tests { fn transcript_layout_matches_current_conservative_bound() { let protocol = protocol_shape(vec![64], vec![], 0, 0, 2); let proof = ProofCalldataLayout::from_protocol(&protocol, 0, 10, 3); - let transcript = TranscriptBufferLayout::from_proof_layout(&proof, 0); + let transcript = TranscriptBufferLayout::from_proof_layout(&proof, 0, &[1]); let first_phase_run = 32 + 128 + 32 + 64 * 128 + 32 * 32; assert!(transcript.words * WORD_BYTES >= first_phase_run); @@ -473,4 +505,37 @@ mod tests { 2 * G1_BYTES + (10 + 3) * WORD_BYTES + 32 * WORD_BYTES ); } + + #[test] + fn transcript_layout_covers_multi_phase_advice_before_first_squeeze() { + // Valid shape (see `plan_allows_challenge_phase_beyond_advice_phases`): + // phase 0 has advice but no challenge, so both phases' advices land in + // the same pre-first-squeeze run alongside a large public-input block. + let protocol = protocol_shape(vec![1, 9], vec![], 0, 0, 1); + let proof = ProofCalldataLayout::from_protocol(&protocol, 0, 12, 2); + let num_instances = 86; + let transcript = TranscriptBufferLayout::from_proof_layout(&proof, num_instances, &[0, 1]); + + let word = layout::transcript::WORD_ABSORB_BYTES; + let g1 = layout::transcript::G1_ABSORB_BYTES; + let cushion = layout::transcript::POST_SQUEEZE_CUSHION_WORDS * WORD_BYTES; + + // Bytes actually absorbed before the first squeeze: vk_digest, + // committed_pi, num_instances length word, instance words, then BOTH + // phases' advices (phase 0 owns no challenge). + let true_initial_run = word + g1 + word + num_instances * word + (1 + 9) * g1; + assert!( + transcript.words * WORD_BYTES >= true_initial_run, + "reserved {} bytes < true pre-squeeze run {}", + transcript.words * WORD_BYTES, + true_initial_run + ); + assert_eq!(transcript.initial_run_bytes, true_initial_run + cushion); + + // The old bound counted only phase 0's advice and therefore under-sized + // the buffer below the true run — confirm the fix was load-bearing. + let phase0_only_run = word + g1 + word + num_instances * word + g1; + assert!(phase0_only_run < true_initial_run); + assert!(phase0_only_run + cushion < true_initial_run); + } } diff --git a/proofs/solidity-verifier/src/lowering/artifacts.rs b/proofs/solidity-verifier/src/lowering/artifacts.rs index 4dbf611ed..9baad6fae 100644 --- a/proofs/solidity-verifier/src/lowering/artifacts.rs +++ b/proofs/solidity-verifier/src/lowering/artifacts.rs @@ -69,6 +69,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { /// Build the Askama model for the main Solidity verifier contract from an /// already-converged lowering plan. + #[allow(clippy::too_many_arguments)] pub(crate) fn generate_verifier_from_plan( &self, plan: &LoweringPlan, @@ -77,6 +78,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { gas_checkpoints: bool, external_quotient: bool, expected_quotient: Option<(usize, U256)>, + provenance: Option<[u8; 32]>, ) -> Halo2Verifier { assert!( expected_quotient.is_none() || external_quotient, @@ -101,6 +103,39 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { U256::from_be_bytes(digest) }); let vk_len = plan.vk.len(); + // BUILD_ID (P10/L-8): one on-chain constant that identifies the build. + // Preimage: domain tag, length-prefixed feature profile (from + // build.rs), vk_digest, expected VK codehash (zero when embedded), + // SRS fingerprint, and the optional deployment provenance tag. + // Deployment records must publish these components so third parties + // can recompute the id; see + // docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + let build_id = { + let vk_digest = plan + .vk + .constants + .iter() + .find(|(name, _)| *name == "vk_digest") + .expect("VK header always carries vk_digest") + .1; + let features = env!("SOLIDITY_VERIFIER_FEATURES"); + let mut hasher = Keccak256::new(); + hasher.update(b"halo2-solidity-verifier-build-v1"); + hasher.update((features.len() as u64).to_be_bytes()); + hasher.update(features.as_bytes()); + hasher.update(vk_digest.to_be_bytes::<32>()); + hasher.update(expected_vk_codehash.unwrap_or_default().to_be_bytes::<32>()); + hasher.update(self.srs_fingerprint()); + match provenance { + Some(tag) => { + hasher.update([1u8]); + hasher.update(tag); + } + None => hasher.update([0u8]), + } + let digest: [u8; 32] = hasher.finalize().into(); + U256::from_be_bytes(digest) + }; let (expected_quotient_len, expected_quotient_codehash) = expected_quotient .map(|(len, codehash)| (Some(len), Some(codehash))) .unwrap_or((None, None)); @@ -157,6 +192,19 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { let num_fixed_bases = fixed_scalar_count .checked_sub(1 + num_perm_bases) .expect("accumulator fixed scalar count is smaller than -G + permutations"); + // Each generated base below is `fixed_comm_mptr + i * 0x80`, + // so more bases than the VK has fixed commitments would + // point past the region into the permutation commitments + // and then the rest of the VK payload. SolidityGenerator:: + // try_new rejects this shape with a typed error; fail + // closed here too, since this is where the out-of-region + // pointers would actually be emitted. + assert!( + num_fixed_bases <= plan.vk.fixed_comms.len(), + "accumulator fixed-base count {num_fixed_bases} exceeds the VK \ + fixed-commitment region ({} commitments)", + plan.vk.fixed_comms.len() + ); std::iter::once(("-G".to_string(), g1_base_mptr_byte, true)) .chain((0..num_fixed_bases).map(|i| { @@ -175,7 +223,10 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { })) .collect::>() }; - debug_assert_eq!( + // Holds by construction of `bases` above; kept as a cheap + // guard on the one-to-one correspondence with the scalars the + // generated verifier reads from calldata. + assert_eq!( bases.len(), fixed_scalar_count, "accumulator fixed-base scalar tail must match generated bases" @@ -213,11 +264,29 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { quotient_limb7_helper: quotient_helper_flags.limb7, quotient_wide_limb7_helper: quotient_helper_flags.wide_limb7, constructor_g1msm_smoke_input_bytes: plan.memory.constructor_g1msm_smoke_input_bytes, + constructor_g1msm_smoke_gas: { + let smoke_bytes = plan.memory.constructor_g1msm_smoke_input_bytes; + assert_eq!( + smoke_bytes % layout::G1_MSM_PAIR_BYTES, + 0, + "constructor G1MSM smoke input is not a whole number of pairs" + ); + layout::gas::g1msm_gas(smoke_bytes / layout::G1_MSM_PAIR_BYTES) + }, + acc_rhs_msm_gas: if expected_has_accumulator { + // Worst case: carried RHS point plus every fixed-base tail + // scalar nonzero. Zero scalars are omitted at runtime, which + // only shrinks the MSM below this bound. + layout::gas::g1msm_gas(1 + acc_fixed_bases.len()) + } else { + 0 + }, limb7_yul_coeffs: LIMB7_YUL_COEFFS, wide_limb7_yul_coeffs: WIDE_LIMB7_YUL_COEFFS, fr_delta: fr_delta_literal(), embedded_vk: (!separate).then(|| plan.vk.clone()), expected_vk_codehash, + build_id, vk_len, num_instances: self.num_instances, k: self.vk.get_domain().k() as usize, diff --git a/proofs/solidity-verifier/src/lowering/calldata.rs b/proofs/solidity-verifier/src/lowering/calldata.rs index 0a3a462bc..3bafa14af 100644 --- a/proofs/solidity-verifier/src/lowering/calldata.rs +++ b/proofs/solidity-verifier/src/lowering/calldata.rs @@ -6,6 +6,7 @@ //! big-endian scalar words. This module performs that deterministic boundary //! conversion. +use ff::PrimeField; use group::GroupEncoding; use midnight_curves::{Fq, G1Affine}; @@ -84,11 +85,26 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { out.extend_from_slice(&y_be[16..48]); Ok(()) }; - let push_scalar_be = |cursor: &mut usize, out: &mut Vec| { - out.extend_from_slice(&scalar_le_to_be_word( - &compressed[*cursor..*cursor + layout::WORD_BYTES], - )); + let push_scalar_be = |cursor: &mut usize, out: &mut Vec| -> Result<(), RepackError> { + let le = &compressed[*cursor..*cursor + layout::WORD_BYTES]; + // The generated verifier reverts on any eval or q_eval word that + // is not a canonical Fr element, so reject it here rather than + // handing the caller calldata that is guaranteed to revert + // on-chain. Mirrors the G1 validation in `push_g1`. + let mut arr = [0u8; layout::WORD_BYTES]; + arr.copy_from_slice(le); + let repr = ::Repr::from(arr); + if Option::::from(Fq::from_repr(repr)).is_none() { + let mut be = arr; + be.reverse(); + return Err(RepackError::NonCanonicalScalar { + offset: *cursor, + bytes_hex: hex::encode(be), + }); + } + out.extend_from_slice(&scalar_le_to_be_word(le)); *cursor += layout::WORD_BYTES; + Ok(()) }; for &n in &plan.g1_groups { for _ in 0..n { @@ -98,13 +114,13 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { // evals (Fr 32-byte LE in native proof) -> BE calldata words // (incl. dummy slots). for _ in 0..plan.num_evals { - push_scalar_be(&mut cursor, &mut out); + push_scalar_be(&mut cursor, &mut out)?; } // f_com push_g1(&mut cursor, &mut out)?; // q_evals (Fr 32-byte LE in native proof) -> BE calldata words. for _ in 0..plan.num_point_sets { - push_scalar_be(&mut cursor, &mut out); + push_scalar_be(&mut cursor, &mut out)?; } // pi push_g1(&mut cursor, &mut out)?; diff --git a/proofs/solidity-verifier/src/lowering/encoding/mod.rs b/proofs/solidity-verifier/src/lowering/encoding/mod.rs index 5ddf9bcdf..8918484ec 100644 --- a/proofs/solidity-verifier/src/lowering/encoding/mod.rs +++ b/proofs/solidity-verifier/src/lowering/encoding/mod.rs @@ -13,6 +13,7 @@ use std::{ }; use ff::PrimeField; +use group::prime::PrimeCurveAffine; use itertools::{izip, Itertools}; use midnight_curves::{Coordinates, CurveAffine, Fq, G1Affine, G2Affine}; use midnight_proofs::plonk::{Any, Column, ConstraintSystem}; @@ -137,10 +138,25 @@ impl ConstraintSystemMeta { /// verifier *reads* from the proof transcript (vs. computes locally /// via Lagrange interpolation). For the poseidon example this is 0; /// for IVC-style fixtures with committed inputs it would be > 0. + /// + /// Panics if the constraint system is outside the supported verifier shape. + /// Use [`ConstraintSystemMeta::try_new`] on fallible paths. Production + /// codegen goes through `SolidityGenerator::try_new`, which promises a + /// typed error, so this panicking form is test-only. + #[cfg(test)] pub(crate) fn new(cs: &ConstraintSystem, nb_committed_instances: usize) -> Self { - let protocol = ProtocolPlan::from_constraint_system(cs, nb_committed_instances); + Self::try_new(cs, nb_committed_instances) + .unwrap_or_else(|err| panic!("invalid protocol plan: {err}")) + } - Self { + /// Fallible counterpart of the test-only `ConstraintSystemMeta::new`. + pub(crate) fn try_new( + cs: &ConstraintSystem, + nb_committed_instances: usize, + ) -> Result { + let protocol = ProtocolPlan::try_from_constraint_system(cs, nb_committed_instances)?; + + Ok(Self { protocol: protocol.clone(), num_fixeds: protocol.num_fixeds, permutation_columns: protocol.permutation_columns.clone(), @@ -163,7 +179,7 @@ impl ConstraintSystemMeta { advice_indices: protocol.advice_indices.clone(), challenge_indices: protocol.challenge_indices.clone(), rotation_last: protocol.rotation_last, - } + }) } /// Check legacy scalar fields against the typed protocol plan. @@ -249,7 +265,9 @@ pub(crate) struct Data { /// User challenge words. pub(crate) challenges: Vec, - /// Locally-computed non-committed instance evaluation. + /// Locally-computed non-committed instance evaluation at `Rotation::cur()`. + /// `SolidityGenerator::try_new` rejects rotated instance queries, so this + /// stays a single word instead of a `(column, rotation)` map. pub(crate) instance_eval: Word, /// Per-(committed-instance-column, rotation): the calldata word for /// that committed instance evaluation. Empty when @@ -273,8 +291,7 @@ pub(crate) struct Data { pub(crate) computed_quotient_eval: Word, /// Word offset (in the verifier's static memory map) of the start of - /// the per-category EIP-2537-padded commitment region. See the - /// `KNOWN BUG` block in `Data::new` for the layout convention. + /// the per-category EIP-2537-padded commitment region. pub(crate) comms_mptr_base: Ptr, /// Memory base of the decoded-evals buffer (Optimisation H3). The /// transcript-side `evaluations` loop spills the decoded scalar value to @@ -285,7 +302,9 @@ pub(crate) struct Data { /// fewer-point-sets path. Empty when the feature is disabled. The /// dummy buffer is laid out immediately after the main reversed- /// evals buffer; `dummy_eval_words[i]` points at - /// `REVERSED_EVALS_MPTR + (num_evals + i) * 0x20`. The transcript + /// `REVERSED_EVALS_MPTR + (num_main_evals + i) * 0x20`, where + /// `num_main_evals` is the eval count *before* dummies are appended + /// (not `meta.num_evals`, which already includes them). The transcript /// loop reads `num_dummy_evals` extra Fr scalars after the main /// eval block and spills them into this buffer the same way the /// main loop does. @@ -306,6 +325,27 @@ impl Data { // BLS12-381 G1 commitments occupy 4 words (EIP-2537 padded), so the // stride between consecutive points is 4 instead of the BN254-era 2. let fixed_comm_mptr = memory.vk_mptr + vk.constants.len(); + // The fixed-commitment region is consumed as meta.num_fixeds slots + // (EcPoint::range(fixed_comm_mptr).take(meta.num_fixeds) below), but the + // permutation base is advanced past vk.fixed_comms.len() of them. If the + // two counts ever diverge, the fixed and permutation commitment regions + // would silently overlap or leave a gap, so require them equal here. + assert_eq!( + vk.fixed_comms.len(), + meta.num_fixeds, + "VK fixed commitment count must match constraint-system fixed count" + ); + // Same argument for the permutation region: `permutation_comms` below + // zips `meta.permutation_columns` against `EcPoint::range(...)`, and + // `izip!` silently truncates to the shorter side. A VK carrying fewer + // commitments than the constraint system has permutation columns would + // therefore drop columns from the permutation argument rather than + // fail. + assert_eq!( + vk.permutation_comms.len(), + meta.permutation_columns.len(), + "VK permutation commitment count must match constraint-system permutation column count" + ); let permutation_comm_mptr = fixed_comm_mptr + G1_WORDS * vk.fixed_comms.len(); let challenge_mptr = memory.challenge_mptr; let theta_mptr = memory.theta_mptr; @@ -579,11 +619,17 @@ pub(crate) enum Location { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Value { - /// Byte offset stored as signed so that the BLS code-gen can compute - /// `ptr - N` even when `N` exceeds the original offset (the result - /// only ever appears as `ptr_end` in `lt(ptr_end, ptr)` style loops - /// where any value strictly less than the smallest visited address is - /// acceptable). + /// Byte offset. Stored as signed for historical reasons; concrete + /// offsets must be non-negative and `Display` panics otherwise. + /// + /// A negative offset has no correct rendering here. It used to be + /// emitted as `sub(0, N)`, which in EVM unsigned arithmetic is + /// `2^256 - N` -- the *largest* representable word. The previous + /// comment claimed such a value was safe because it "only ever appears + /// as `ptr_end` in `lt(ptr_end, ptr)` style loops where any value + /// strictly less than the smallest visited address is acceptable", but + /// `lt` is unsigned, so it is larger than every address and such a loop + /// runs zero iterations. Integer(isize), /// A symbolic Yul identifier `name`, with an optional byte-offset that /// will be rendered as `add(name, 0xNN)` (or just `name` when zero). @@ -594,7 +640,19 @@ impl Value { /// Return the concrete offset, panicking for symbolic identifiers. pub(crate) fn as_usize(&self) -> usize { match self { - Value::Integer(int) => *int as usize, + Value::Integer(int) => { + // `Integer` is signed only so BLS pointer math can produce + // negative `ptr_end` sentinels for `lt(ptr_end, ptr)` loops. A + // concrete memory address is never negative; `*int as usize` on + // a negative value would wrap to a near-2^word offset and + // silently corrupt every derived mload/mstore, so fail closed. + assert!( + *int >= 0, + "Value::as_usize on negative offset {int}: signed offsets are \ + only valid as lt()-loop sentinels, not concrete addresses" + ); + *int as usize + } Value::Identifier(..) => unreachable!(), } } @@ -636,7 +694,9 @@ impl Display for Value { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Value::Integer(int) if *int >= 0 => write!(f, "{}", fmt_hex(*int)), - Value::Integer(int) => write!(f, "sub(0, {})", fmt_hex(-*int)), + Value::Integer(int) => { + panic!("negative pointer offset {int} has no correct Yul rendering") + } Value::Identifier(ident, 0) => write!(f, "{ident}"), Value::Identifier(ident, off) if *off > 0 => { write!(f, "add({ident}, {})", fmt_hex(*off)) @@ -648,29 +708,29 @@ impl Display for Value { } } -impl Add for Value { - /// Pointer-expression value after word-wise addition. +impl Sub for Value { + /// Pointer-expression value after word-wise subtraction. type Output = Value; - /// Advance by `rhs` EVM words. - fn add(self, rhs: usize) -> Self::Output { + /// Move backward by `rhs` EVM words. + fn sub(self, rhs: usize) -> Self::Output { match self { - Value::Integer(int) => Value::Integer(int + (rhs as isize) * WORD_BYTES as isize), + Value::Integer(int) => Value::Integer(int - (rhs as isize) * WORD_BYTES as isize), Value::Identifier(name, off) => { - Value::Identifier(name, off + (rhs as isize) * WORD_BYTES as isize) + Value::Identifier(name, off - (rhs as isize) * WORD_BYTES as isize) } } } } -impl Sub for Value { - /// Pointer-expression value after word-wise subtraction. +impl Add for Value { + /// Pointer-expression value after word-wise addition. type Output = Value; - /// Move backward by `rhs` EVM words. - fn sub(self, rhs: usize) -> Self::Output { + /// Advance by `rhs` EVM words. + fn add(self, rhs: usize) -> Self::Output { match self { - Value::Integer(int) => Value::Integer(int - (rhs as isize) * WORD_BYTES as isize), + Value::Integer(int) => Value::Integer(int + (rhs as isize) * WORD_BYTES as isize), Value::Identifier(name, off) => { - Value::Identifier(name, off - (rhs as isize) * WORD_BYTES as isize) + Value::Identifier(name, off + (rhs as isize) * WORD_BYTES as isize) } } } @@ -719,16 +779,6 @@ impl Display for Ptr { } } -impl Add for Ptr { - /// Pointer with the same location and advanced value. - type Output = Ptr; - /// Advance by `rhs` EVM words while preserving location. - fn add(mut self, rhs: usize) -> Self::Output { - self.value = self.value + rhs; - self - } -} - impl Sub for Ptr { /// Pointer with the same location and rewound value. type Output = Ptr; @@ -739,6 +789,16 @@ impl Sub for Ptr { } } +impl Add for Ptr { + /// Pointer with the same location and advanced value. + type Output = Ptr; + /// Advance by `rhs` EVM words while preserving location. + fn add(mut self, rhs: usize) -> Self::Output { + self.value = self.value + rhs; + self + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct Word(Ptr); @@ -795,7 +855,7 @@ impl EcPoint { /// Infinite iterator of consecutive padded G1 points. pub(crate) fn range(base: impl Into) -> impl Iterator { let base = base.into().base; - (0..).map(move |idx| EcPoint::new(base + 4 * idx)) + (0..).map(move |idx| EcPoint::new(base + G1_WORDS * idx)) } /// Return the pointer to the first word. @@ -832,8 +892,18 @@ fn fp48_be_to_hi_lo(be: &[u8]) -> (U256, U256) { /// big-endian and split (hi=top 16 bytes padded into u256, lo=bottom 32 /// bytes). pub(crate) fn g1_to_u256s(ec_point: impl Borrow) -> [U256; 4] { - let Some(coords) = Option::>::from(ec_point.borrow().coordinates()) - else { + let point = ec_point.borrow(); + let Some(coords) = Option::>::from(point.coordinates()) else { + // `coordinates()` returns None for the identity *and* for any off-curve + // point. The all-zero words below are the EIP-2537 encoding of the point + // at infinity, so silently returning them for an off-curve input would + // bake an identity commitment into the verifier. Only the identity may + // take this path; anything else is a malformed/corrupted point and must + // fail codegen rather than degrade a soundness-critical constant. + assert!( + bool::from(point.is_identity()), + "refusing to EIP-2537 encode an off-curve G1 point as the identity" + ); return [U256::ZERO; 4]; }; let mut x_be = [0u8; BLS_FP_BYTES]; @@ -854,8 +924,16 @@ pub(crate) fn g1_to_u256s(ec_point: impl Borrow) -> [U256; 4] { /// in big-endian per coord. The midnight-curves convention matches: /// each `Fp` coordinate read via `to_repr()` returns LE bytes. pub(crate) fn g2_to_u256s(ec_point: impl Borrow) -> [U256; 8] { - let Some(coords) = Option::>::from(ec_point.borrow().coordinates()) - else { + let point = ec_point.borrow(); + let Some(coords) = Option::>::from(point.coordinates()) else { + // See `g1_to_u256s`: `coordinates()` also returns None for off-curve G2 + // points, whose all-zero encoding would collapse to the identity (e.g. + // a corrupted `s_g2` degenerating the `e(w, -s*G2)` pairing term). Only + // the genuine identity may be encoded as zeros. + assert!( + bool::from(point.is_identity()), + "refusing to EIP-2537 encode an off-curve G2 point as the identity" + ); return [U256::ZERO; 8]; }; diff --git a/proofs/solidity-verifier/src/lowering/kzg/mod.rs b/proofs/solidity-verifier/src/lowering/kzg/mod.rs index 0fb662402..c97cdfa71 100644 --- a/proofs/solidity-verifier/src/lowering/kzg/mod.rs +++ b/proofs/solidity-verifier/src/lowering/kzg/mod.rs @@ -46,6 +46,7 @@ use crate::lowering::{ abi::proof::ProofCalldataLayout, encoding::{ConstraintSystemMeta, Data, EcPoint, Location, Ptr, Word}, layout::{ + gas, memory::{ FinalMsmShape, PcsMemoryRequirements, VerifierMemoryLayout, G1ADD_INPUT_BYTES, G1_BYTES, G1_MSM_PAIR_BYTES, PCS_STATIC_WORKING_WORDS, WORD_BYTES, @@ -93,12 +94,143 @@ impl Query { /// simple multiplicative selector queries are skipped because the custom /// linearization query carries their commitments and selector accumulators. pub(crate) fn queries(meta: &ConstraintSystemMeta, data: &Data) -> Vec { - meta.protocol + let queries: Vec = meta + .protocol .pcs_queries .iter() .copied() .map(|source| query_from_plan(source, meta, data)) - .collect() + .collect(); + // L-2 (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): downstream MSM + // emission silently omits commitments pinned to `G1_IDENTITY_MPTR` while + // keeping their evaluation terms. That omission is sound ONLY because + // the identity pointer is reserved for the committed-instance column + // (`SUPPORTED_COMMITTED_INSTANCE_COMMITMENT` is the identity, absorbed + // as 128 zero bytes). Pin that justification here, where each query + // still knows its provenance: any other query source acquiring the + // identity pointer would make the emitters drop a real MSM term. + // + // MF-8: the visible consequence downstream is a set whose eval-term count + // exceeds its commitment-term count by one (e.g. `q_eval_set[0]: 43 + // evaluation term(s), 42 commitment term(s)` in the IVC render). That is + // not an off-by-one. The omitted commitment is the identity, so the + // multi-open equation still holds exactly -- and the omission is what + // FORCES the committed-instance eval to zero: the eval stays in the + // batched claim with coefficient trunc(x1^i) while contributing nothing + // to the commitment side, so any nonzero value breaks the opening. The + // enforcement is therefore indirect (via batching, per-term error + // 2^-128), not an equality check anywhere in the verifier. + let g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); + for (source, query) in meta.protocol.pcs_queries.iter().zip(&queries) { + assert!( + query.comm != g1_identity || matches!(source, PcsQuerySource::CommittedInstance(_)), + "query {source:?} resolves to the G1 identity commitment; only \ + committed-instance queries may be identity-pinned, since the MSM \ + emitters omit identity commitments while keeping their evals" + ); + } + assert_permutation_query_order_is_upstream_equivalent(meta, data, &queries); + queries +} + +/// I-6 (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): the midnight-proofs +/// verifier emits permutation z queries as all `(Cur, Next)` pairs in +/// forward set order followed by the `Last` openings in REVERSE set order, +/// while this generator interleaves `Cur/Next/Last` per set. For every +/// supported shape the two orderings happen to produce identical +/// intermediate point-set structure -- but that is a property of the +/// concrete plan (no new rotation or commitment introduced between the +/// placements), not an order-insensitive transformation. Re-derive the +/// intermediate sets under the upstream ordering and refuse to plan a +/// circuit where the structures diverge, since every x1-power index and MSM +/// slot downstream depends on it. +fn assert_permutation_query_order_is_upstream_equivalent( + meta: &ConstraintSystemMeta, + data: &Data, + generator_order: &[Query], +) { + let sources = &meta.protocol.pcs_queries; + let z_positions: Vec = sources + .iter() + .enumerate() + .filter(|(_, s)| matches!(s, PcsQuerySource::PermutationZ { .. })) + .map(|(i, _)| i) + .collect(); + if z_positions.is_empty() { + return; + } + assert!( + z_positions.windows(2).all(|w| w[1] == w[0] + 1), + "permutation z queries are expected to form one contiguous block" + ); + + // Rebuild the z block in the upstream order: (Cur, Next) per set in + // forward order, then Last per set in reverse order. + let z_sources: Vec = z_positions.iter().map(|&i| sources[i]).collect(); + let mut upstream_block: Vec = z_sources + .iter() + .copied() + .filter(|s| { + matches!( + s, + PcsQuerySource::PermutationZ { + kind: PermutationZEval::Cur | PermutationZEval::Next, + .. + } + ) + }) + .collect(); + let mut last_queries: Vec = z_sources + .iter() + .copied() + .filter(|s| { + matches!( + s, + PcsQuerySource::PermutationZ { + kind: PermutationZEval::Last, + .. + } + ) + }) + .collect(); + last_queries.reverse(); + upstream_block.extend(last_queries); + + let mut upstream_sources = sources.clone(); + for (slot, source) in z_positions.iter().zip(upstream_block) { + upstream_sources[*slot] = source; + } + let upstream_queries: Vec = upstream_sources + .iter() + .copied() + .map(|source| query_from_plan(source, meta, data)) + .collect(); + + let generator_sets = construct_intermediate_sets_impl(generator_order); + let upstream_sets = construct_intermediate_sets_impl(&upstream_queries); + assert_eq!( + generator_sets.point_sets, upstream_sets.point_sets, + "permutation z query interleaving changes the KZG point-set structure \ + relative to the upstream (Cur/Next then reversed Last) ordering; the \ + generated x1-power indices would not match the native verifier" + ); + assert_eq!( + generator_sets.commitments.len(), + upstream_sets.commitments.len(), + "permutation z query interleaving changes the deduplicated commitment \ + count relative to the upstream ordering" + ); + for (g, u) in generator_sets.commitments.iter().zip(&upstream_sets.commitments) { + assert!( + g.set_index == u.set_index && g.comm == u.comm && g.evals == u.evals, + "permutation z query interleaving reorders commitment {:?} relative \ + to the upstream ordering (set {} vs {}); MSM slots and x1 powers \ + would diverge from the native verifier", + g.comm, + g.set_index, + u.set_index, + ); + } } /// Resolve a typed protocol query source to concrete commitment/eval handles. @@ -320,6 +452,23 @@ fn construct_intermediate_sets_impl(queries: &[Query]) -> IntermediateSets { // when their evals also agree; otherwise the proof claims the // same polynomial opens to two different values, which is a // protocol bug and must be rejected. + // + // TODO(structural): this dedup and the whole intermediate-set + // grouping compare EcPoint/Word handles by *memory pointer* (derived + // PartialEq), not by runtime value, whereas the midnight-proofs + // prover groups queries by polynomial identity and the verifier by + // commitment value. They agree today only because (a) + // SolidityGenerator supports a single committed-instance column (see + // validate_instance_column_shape in builder/api.rs), and (b) every + // downstream consumer depends only on first-appearance order and + // per-commitment point sets. The assert below also compares eval + // Words by pointer: with >= 2 committed-instance columns sharing + // G1_IDENTITY_MPTR at the same rotation it would panic at codegen + // (their eval Words are distinct memory handles) instead of + // collapsing them, and compute_dummy_queries would silently `skip` + // the same pair. Grouping by runtime value would lift both + // restrictions but is a structural change; the invariant is + // documented here rather than fixed. if let Some(existing_pos) = slot.1.iter().position(|pi| *pi == point_idx) { assert_eq!( slot.2[existing_pos], query.eval, @@ -744,9 +893,17 @@ pub(crate) fn memory_requirements( ) -> PcsMemoryRequirements { let sets = intermediate_sets(meta, data); let n_sets = sets.point_sets.len(); - if n_sets == 0 { - return PcsMemoryRequirements::default(); - } + // Fail closed: zero point sets means the plan carries no PCS queries, which + // would size (and, in `computations`, emit) a verifier with no final + // pairing check. Since zero-initialized PAIRING_{LHS,RHS}_MPTR encode the + // point at infinity, such a verifier accepts any transcript-parseable proof. + // `ProtocolPlan::validate` guarantees at least the Linearization query, so + // this is unreachable; assert it rather than silently return a default. + assert!( + n_sets != 0, + "KZG intermediate-set construction produced zero point sets; refusing to \ + size a verifier with no PCS/pairing check (would be accept-all)" + ); let by_set = commitments_by_set(&sets, n_sets); let commitments_per_set = by_set.iter().map(Vec::len); @@ -819,9 +976,17 @@ pub(crate) fn computations( const TRUNC_MASK_128: &str = "0xffffffffffffffffffffffffffffffff"; let sets = intermediate_sets(meta, data); let n_sets = sets.point_sets.len(); - if n_sets == 0 { - return Vec::new(); - } + // Fail closed: an empty point-set list would emit a verifier with no Block 6, + // so PAIRING_{LHS,RHS}_MPTR stay zero-initialized. Zero memory is the EIP-2537 + // encoding of the BLS12-381 point at infinity, so the final pairing evaluates + // to 1 and the verifier accepts ANY proof with no cryptographic checking. + // `ProtocolPlan::validate` requires the query schedule to end with the + // Linearization query (n_sets >= 1), so reaching here is a generator bug. + assert!( + n_sets != 0, + "KZG intermediate-set construction produced zero point sets; refusing to \ + emit a verifier with no PCS/pairing check (would be accept-all)" + ); // The emitted blocks below adapt the Rust `multi_prepare` flow: // construct/sort point sets, fold q_eval vectors, interpolate at x3, @@ -871,6 +1036,26 @@ pub(crate) fn computations( let max_rot = *distinct_rotations.iter().max().unwrap_or(&0); let min_rot = *distinct_rotations.iter().min().unwrap_or(&0); + // Fail closed on pathological rotation magnitudes. This block unrolls one + // `mulmod` per unit step across the entire rotation span (forward to + // max_rot, backward to min_rot), so the emitted line count scales with + // |max_rot| + |min_rot|, NOT with the (separately capped) number of + // distinct rotations. A circuit using a very large rotation would emit + // enough Yul to exceed the EIP-170 24KB runtime-code limit and produce an + // undeployable verifier with no diagnostic from our own validation. Bound + // the walk here; if this ever fires for a legitimate circuit, roll the + // walk into a Yul loop (as Block 2 does for x1 powers) rather than raising + // the cap. The cap is far above any realistic circuit's rotation range. + const MAX_ROTATION_WALK_STEPS: i64 = 4096; + let walk_steps = i64::from(max_rot).max(0) + (-i64::from(min_rot)).max(0); + assert!( + walk_steps <= MAX_ROTATION_WALK_STEPS, + "PCS rotation-point walk would unroll {walk_steps} mulmod steps \ + (max_rot={max_rot}, min_rot={min_rot}), exceeding the \ + {MAX_ROTATION_WALK_STEPS}-step cap; such a verifier would blow the \ + EIP-170 runtime code-size limit (roll the walk into a Yul loop instead)" + ); + let store_rot = |rot: i32| -> Option { distinct_rotations.iter().position(|r| *r == rot).map(|idx| { format!( @@ -1029,10 +1214,18 @@ pub(crate) fn computations( // ------------------------------------------------------------------ let eval_src_table_mptr: usize = memory.pcs_q_eval_source_table_mptr; + let comment_g1_identity = EcPoint::new(Ptr::memory("G1_IDENTITY_MPTR")); for (set_idx, commitments_in_set) in by_set.iter().enumerate() { let mut lines: Vec = Vec::new(); let q_eval_base = format!("add(Q_EVAL_SET_MPTR, {:#x})", set_idx * WORD_BYTES); let m = commitments_in_set.len(); + // The MSM emission (blocks 3 and 5) skips commitments pinned to + // the G1 identity while their evaluation contribution stays in + // q_eval_set, so the two counts differ whenever a set carries an + // identity commitment. Report both, or the emitted comment + // contradicts the pair count of the MSM right below it (L-2/P7). + let commitment_terms = + commitments_in_set.iter().filter(|c| c.comm != comment_g1_identity).count(); // q_eval_set[s] is itself a *vector* of |set| evaluations // (not a single scalar): one per rotation in the set's @@ -1053,7 +1246,8 @@ pub(crate) fn computations( if q_eval_strategy(commitments_in_set) == QEvalStrategy::Rolled { // -------- Rolled path (Opt I + Opt J merged) ---------- lines.push(format!( - "// q_eval_set[{set_idx}]: {m} commitment(s) (rolled, m>={Q_EVAL_ROLL_THRESHOLD})" + "// q_eval_set[{set_idx}]: {m} evaluation term(s), {commitment_terms} \ + commitment term(s) (rolled, m>={Q_EVAL_ROLL_THRESHOLD})" )); // 1. Pre-stage source-eval addresses at EVAL_SRC_TABLE_MPTR. Layout: row-major @@ -1127,7 +1321,10 @@ pub(crate) fn computations( } } else { // -------- Unrolled path (preserved for non-rolled sets) -- - lines.push(format!("// q_eval_set[{set_idx}]: {m} commitment(s)")); + lines.push(format!( + "// q_eval_set[{set_idx}]: {m} evaluation term(s), {commitment_terms} \ + commitment term(s)" + )); // Fr-only eval accumulation in stack locals. for (k, ev) in first.evals.iter().enumerate() { @@ -1189,7 +1386,7 @@ pub(crate) fn computations( )); lines.push(format!( "trace_point({}, {trace_scratch:#x})", - 40000 + set_idx + trace::PCS_Q_COM_BASE + set_idx as u64 )); continue; } @@ -1265,18 +1462,22 @@ pub(crate) fn computations( ); let msm_len = non_identity_terms * G1_MSM_PAIR_BYTES; lines.push(format!( - "let q_com_trace_ok_{set_idx} := staticcall(gas(), 0x0c, {trace_scratch:#x}, {msm_len:#x}, {trace_scratch:#x}, {G1_BYTES:#x})" + "// exact EIP-2537 G1MSM cost for {non_identity_terms} pair(s)" + )); + lines.push(format!( + "let q_com_trace_ok_{set_idx} := staticcall({}, 0x0c, {trace_scratch:#x}, {msm_len:#x}, {trace_scratch:#x}, {G1_BYTES:#x})", + gas::g1msm_gas(non_identity_terms) )); lines.push(format!( "q_com_trace_ok_{set_idx} := and(q_com_trace_ok_{set_idx}, eq(returndatasize(), {G1_BYTES:#x}))" )); lines.push(format!( "if iszero(q_com_trace_ok_{set_idx}) {{ mstore(TRACE_U256_MPTR, {}) revert(TRACE_U256_MPTR, {WORD_BYTES:#x}) }}", - 40000 + set_idx + trace::PCS_Q_COM_BASE + set_idx as u64 )); lines.push(format!( "trace_point({}, {trace_scratch:#x})", - 40000 + set_idx + trace::PCS_Q_COM_BASE + set_idx as u64 )); } blocks.push(lines); @@ -1399,11 +1600,27 @@ pub(crate) fn computations( // Soundness: requires every input to be non-zero. dx_j is // non-zero by Fiat-Shamir (x3 is uniform random; the // probability that x3 = p_j for a structured rotation point - // is ~2^-256). lbasis_j is non-zero because the points in a - // set are distinct by construction (`construct_intermediate_sets` - // de-duplicates rotations within each set). Defensive note: - // a malicious prover cannot influence either, so we don't - // need an explicit zero check. + // is ~2^-256). lbasis_j = prod_{k != j} (p_j - p_k) is non-zero + // as long as the rotation points p = x*omega^rot are pairwise + // distinct within the set. `construct_intermediate_sets` + // de-duplicates rotation *values* (i32) per set, and distinct + // values map to distinct points only because the domain order + // n = 2^k exceeds the rotation span for every supported circuit + // (rotations are bounded by the gate/lookup structure, k is + // large). A malicious prover cannot influence either value, so + // no explicit codegen zero check is added here. + // + // Note the failure mode is fail-closed, not silent: if a + // degenerate tiny-domain circuit ever aliased two rotations + // (rot_i == rot_j mod n), the corresponding p_j - p_k would be + // zero, so some lbasis_j and hence the batched product bp_{n-1} + // would be zero, and `scalar_inv` reverts on a zero input + // (AssemblyHelpers.yul). Such a verifier rejects all proofs + // rather than computing a wrong f_eval. + // + // TODO: to surface that misconfiguration at codegen time instead + // of at proof time, thread the domain order n into this emitter + // and assert every point set's rotation span is < n. // // The reference computes lagrange interpolation directly via // full polynomial construction; here we collapse the @@ -1534,6 +1751,14 @@ pub(crate) fn computations( let final_msm_shape = final_msm_shape(meta, data, &by_set); let final_msm_terms = final_msm_shape.terms; let final_msm_len = final_msm_shape.input_bytes; + // The forwarded gas bound below is derived from the term count, so + // the byte length handed to the precompile must be exactly that many + // whole pairs -- a mismatch would under-fund the MSM. + assert_eq!( + final_msm_terms * G1_MSM_PAIR_BYTES, + final_msm_len, + "final MSM input byte length is not a whole number of MSM pairs" + ); let final_msm_scratch = memory.pcs_final_msm_scratch_mptr; lines.push("// build final_com and v (KZG single-opening proof, fused MSM)".to_string()); @@ -1675,7 +1900,11 @@ pub(crate) fn computations( lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall(gas(), 0x0c, {final_msm_scratch:#x}, {:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})", + " // exact EIP-2537 G1MSM cost for {final_msm_terms} pair(s)" + )); + lines.push(format!( + " success := staticcall({}, 0x0c, {final_msm_scratch:#x}, {:#x}, FINAL_COM_MPTR, {G1_BYTES:#x})", + gas::g1msm_gas(final_msm_terms), final_msm_len )); lines.push(format!( @@ -1708,7 +1937,12 @@ pub(crate) fn computations( lines.push("// Scale z*pi - vG before the final pairing check".to_string()); lines.push("// pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi)".to_string()); - // PAIRING_LHS = pi (paired against G2_BASE). + // PAIRING_LHS = pi. FinalPairing.yul calls + // ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) with the + // slots swapped, and ec_pairing pairs its first argument against + // G2_BASE and its second against NEG_S_G2_BASE, so pi (PAIRING_LHS) + // is paired against NEG_S_G2_BASE (the [s]_2 side), matching the KZG + // identity e(final_com - v*G + x3*pi, [1]_2) = e(pi, [s]_2). lines.push(format!("mcopy(PAIRING_LHS_MPTR, PI_MPTR, {G1_BYTES:#x})")); // tmp = (-v) * G => load G into planned scratch, scale by (r - v). @@ -1721,7 +1955,7 @@ pub(crate) fn computations( )); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall(gas(), 0x0c, {scratch:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" + " success := staticcall(G1MSM_GAS_1PAIR, 0x0c, {scratch:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1734,7 +1968,7 @@ pub(crate) fn computations( )); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall(gas(), 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" + " success := staticcall(G1ADD_GAS, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1746,7 +1980,7 @@ pub(crate) fn computations( lines.push(format!("mstore({scratch_g1add_scalar:#x}, mload(X3_MPTR))")); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall(gas(), 0x0c, {scratch_g1_b:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch_g1_b:#x}, {G1_BYTES:#x})" + " success := staticcall(G1MSM_GAS_1PAIR, 0x0c, {scratch_g1_b:#x}, {G1_MSM_PAIR_BYTES:#x}, {scratch_g1_b:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" @@ -1754,7 +1988,7 @@ pub(crate) fn computations( lines.push("}".to_string()); lines.push("if success {".to_string()); lines.push(format!( - " success := staticcall(gas(), 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" + " success := staticcall(G1ADD_GAS, 0x0b, {scratch:#x}, {G1ADD_INPUT_BYTES:#x}, {scratch:#x}, {G1_BYTES:#x})" )); lines.push(format!( " success := and(success, eq(returndatasize(), {G1_BYTES:#x}))" diff --git a/proofs/solidity-verifier/src/lowering/layout/memory.rs b/proofs/solidity-verifier/src/lowering/layout/memory.rs index 55bc5acb2..022381f94 100644 --- a/proofs/solidity-verifier/src/lowering/layout/memory.rs +++ b/proofs/solidity-verifier/src/lowering/layout/memory.rs @@ -11,7 +11,7 @@ //! historical addresses in the generated verifier, gives each range a name and //! lifetime, and rejects accidental overlap when two live ranges can coexist. //! Intentional scratch reuse is modeled by assigning the same byte range to -//! disjoint `MemoryPhase`s. +//! disjoint `MemoryPhase`s or non-overlapping phase spans. //! //! This is not a packing allocator yet. The first version is deliberately //! conservative: it names the old layout, validates it, and centralizes all @@ -21,13 +21,13 @@ use std::collections::BTreeMap; pub(crate) use crate::lowering::layout::{ - ACC_MSM_MIN_SCRATCH_BYTES, G1ADD_INPUT_BYTES, G1_BYTES, G1_MSM_PAIR_BYTES, G1_WORDS, - LOW_MEMORY_SCRATCH_START, MODEXP_FRAME_BYTES, MODEXP_SCRATCH_BYTES, - PAIRING_STATIC_WORKING_WORDS, PAIRING_TWO_PAIR_BYTES, PCS_PAIRING_SCRATCH_START, - PCS_STATIC_WORKING_WORDS, QUOTIENT_RETURN_BUFFER_START, SOLIDITY_FREE_MEMORY_POINTER_SLOT, - SOLIDITY_RESERVED_MEMORY_BYTES, SOLIDITY_SCRATCH_SPACE_BYTES, SOLIDITY_ZERO_SLOT, - TRANSCRIPT_BUFFER_START, VERIFIER_RETURN_BUFFER_START, VK_CONSTRUCTOR_PAYLOAD_START, - WORD_BYTES, + accumulator::PAIRING_BATCH_HASH_BYTES, ACC_MSM_MIN_SCRATCH_BYTES, G1ADD_INPUT_BYTES, G1_BYTES, + G1_MSM_PAIR_BYTES, G1_WORDS, LOW_MEMORY_SCRATCH_START, MODEXP_FRAME_BYTES, + MODEXP_SCRATCH_BYTES, PAIRING_STATIC_WORKING_WORDS, PAIRING_TWO_PAIR_BYTES, + PCS_PAIRING_SCRATCH_START, PCS_STATIC_WORKING_WORDS, QUOTIENT_RETURN_BUFFER_START, + SOLIDITY_FREE_MEMORY_POINTER_SLOT, SOLIDITY_RESERVED_MEMORY_BYTES, + SOLIDITY_SCRATCH_SPACE_BYTES, SOLIDITY_ZERO_SLOT, TRANSCRIPT_BUFFER_START, + VERIFIER_RETURN_BUFFER_START, VK_CONSTRUCTOR_PAYLOAD_START, WORD_BYTES, }; use crate::lowering::{ encoding::{ConstraintSystemMeta, Ptr}, @@ -36,11 +36,11 @@ use crate::lowering::{ }; /// Accumulator pairing-batch hash frame. /// -/// The template starts this frame at `0x100`, writes a one-word domain tag, -/// then four G1 points: KZG rhs/lhs and accumulator rhs/lhs. The last copy ends -/// at `0x320`, so the registered range is `[0x100, 0x320)`. -const ACCUMULATOR_PAIRING_BATCH_BYTES: usize = - PAIRING_TWO_PAIR_BYTES - G1ADD_INPUT_BYTES + WORD_BYTES; +/// The template starts this frame at `PAIRING_BATCH_PTR` (`0x1000`), writes a +/// one-word domain tag, then four G1 points: KZG rhs/lhs and accumulator +/// rhs/lhs. The last copy ends `0x220` bytes later, so the registered range is +/// `[0x1000, 0x1220)`. +const ACCUMULATOR_PAIRING_BATCH_BYTES: usize = PAIRING_BATCH_HASH_BYTES; // Fixed word offsets from `THETA_MPTR`. // @@ -118,32 +118,47 @@ impl ThetaWindowLayout { } } +/// Verifier execution phases, in the order the generated code runs them. +/// +/// The derived `Ord` is load-bearing: `MemoryLifetime::intersects` compares +/// phases with `<=` to decide whether a `PhaseSpan` covers a `Phase`, so a +/// variant declared out of runtime order makes the arena's overlap validation +/// answer the wrong question. Keep this list in sync with the include order in +/// `templates/contracts/Halo2Verifier.sol` and the call sites it renders. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub(crate) enum MemoryPhase { /// Generated verifying-key constructor return payload. VkConstructorPayload, /// Constructor-only precompile smoke tests. ConstructorSmoke, + /// Public-accumulator MSM input buffer. + /// + /// `validate_public_accumulator` runs from VkLoading.yul, immediately + /// after the VK payload is loaded and *before* the transcript starts. + AccumulatorMsm, /// Streaming Fiat-Shamir buffer before generated VK memory is live. Transcript, - /// Single scalar inversion scratch used by the modexp wrapper. - ScalarInv, /// Batch inversion for Lagrange denominator terms. LagrangeBatchInvert, /// Compact quotient VM temps and stack. QuotientVm, - /// Historical fixed PCS windows rooted at `ROT_POINTS_MPTR`. - PcsFixed, + /// Trace-only linearization-commitment MSM, run after the quotient VM and + /// before the PCS blocks reuse the same scratch band. + LinearizationTrace, /// Source-address table used by the rolled q_eval fold. PcsQEvalSourceTable, /// Optional trace-only q_com MSM materialization. PcsQComTrace, + /// Single scalar inversion scratch used by the modexp wrapper. + /// + /// `scalar_inv` is called from the PCS f_eval interpolation, i.e. after + /// the q_eval source-table fold and before the fused final MSM -- not + /// during transcript absorption. + ScalarInv, /// Fused final PCS MSM input buffer. PcsFinalMsm, /// Low-memory PCS pairing input helpers. PcsPairing, - /// Public-accumulator MSM input buffer. - AccumulatorMsm, /// Public-accumulator pairing-batch hash and two G1 add/MSM frames. AccumulatorPairingBatch, /// Final two-pair KZG pairing frame. @@ -162,6 +177,11 @@ pub(crate) enum MemoryLifetime { /// Region is live only during the named phase. Regions in different phases /// may reuse the same byte range. Phase(MemoryPhase), + /// Region is written in one phase and read through a later phase. + PhaseSpan { + start: MemoryPhase, + end: MemoryPhase, + }, } impl MemoryLifetime { @@ -170,6 +190,20 @@ impl MemoryLifetime { match (self, other) { (Self::Permanent, _) | (_, Self::Permanent) => true, (Self::Phase(lhs), Self::Phase(rhs)) => lhs == rhs, + (Self::Phase(phase), Self::PhaseSpan { start, end }) + | (Self::PhaseSpan { start, end }, Self::Phase(phase)) => { + start <= phase && phase <= end + } + ( + Self::PhaseSpan { + start: lhs_start, + end: lhs_end, + }, + Self::PhaseSpan { + start: rhs_start, + end: rhs_end, + }, + ) => lhs_start <= rhs_end && rhs_start <= lhs_end, } } } @@ -481,15 +515,27 @@ pub(crate) struct VerifierMemoryLayout { pub(crate) trashcan_comms_mptr_base: Ptr, pub(crate) quotient_limb_comms_mptr_base: Ptr, /// First byte after all decompressed proof commitments. Selector - /// accumulators are live here during final linearization/final MSM. + /// accumulators are written by quotient evaluation and read by PCS MSMs. pub(crate) selector_acc_mptr: usize, /// Reuses selector-accumulator bytes during the earlier Lagrange batch /// inversion phase. pub(crate) batch_invert_scratch_mptr: usize, + /// Batch-inversion input run: denominators, then their in-place inverses, + /// then Lagrange values, distilled into named theta slots at the end of + /// the Lagrange block. + pub(crate) lagrange_denoms_mptr: usize, /// First quotient VM temporary. Also the canonical PCS scratch base once /// selector accumulators are accounted for. pub(crate) quotient_tmp_mptr: usize, pub(crate) quotient_stack_mptr: usize, + /// First address past the quotient VM stack / callback scratch region. + /// + /// MF-3: the interpreter's spill pointer walks upward from + /// `quotient_stack_mptr` with no ceiling of its own, so the rendered VM + /// clamps `q_sp` against this bound. The region is sized for the larger + /// of the interpreted stack depth and the structured native-callback + /// scratch, which is exactly the ceiling both uses must respect. + pub(crate) quotient_stack_hi: usize, pub(crate) pcs_q_eval_source_table_mptr: usize, pub(crate) pcs_q_com_trace_scratch_mptr: usize, pub(crate) pcs_final_msm_scratch_mptr: usize, @@ -604,6 +650,45 @@ impl VerifierMemoryLayout { .max() .expect("constructor G1MSM smoke bounds are non-empty"); let batch_invert_len = batch_invert_scratch_bytes(meta, config.num_instances); + let lagrange_denoms_len = batch_invert_input_words(meta, config.num_instances) * WORD_BYTES; + // P9 (L-7, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): the + // generated Lagrange block computes its batch-inversion run length + // INDEPENDENTLY of the planner, in the template (`Lagrange.yul`: + // one denominator per public instance -- or one fallback slot when + // there are none -- plus `abs(rotation_last)` negative-row + // denominators, plus x_n - 1). `batch_invert_scratch` sits + // immediately below `lagrange_denoms`, sized with ZERO slack, so if + // the two formulas ever drift the forward pass's modexp frame lands + // on denominator[0] and corrupts it SILENTLY: the modexp still + // succeeds, `ret` stays 1, and the backward pass returns wrong + // inverses without reverting. Mirror the template's expression here + // and refuse to plan a layout where they disagree or where the + // scratch cannot hold the run. + { + let n = batch_invert_input_words(meta, config.num_instances); + let neg_lagranges = meta.rotation_last.unsigned_abs() as usize; + let template_run_words = if config.num_instances == 0 { + // fallback denominator slot + negative-row denominators + (x_n - 1) + 1 + neg_lagranges + 1 + } else { + config.num_instances + neg_lagranges + 1 + }; + assert_eq!( + n, template_run_words, + "planner batch-inversion input count ({n} words) does not match the \ + run the generated Lagrange block writes ({template_run_words} words); \ + the scratch/denominator regions would be mis-sized" + ); + let required = n.saturating_sub(2) * WORD_BYTES + MODEXP_FRAME_BYTES; + assert!( + batch_invert_len >= required, + "batch_invert scratch is {batch_invert_len} bytes but n={n} inputs need \ + {required} (n-2 prefix products plus one modexp frame); an overflow \ + lands in lagrange_denoms and silently corrupts denominator[0] -- the \ + modexp still succeeds and the backward pass returns wrong inverses \ + WITHOUT reverting" + ); + } let quotient_return_len = (2 + meta.num_simple_selectors) * WORD_BYTES; let mut arena = MemoryArena::default(); @@ -682,6 +767,35 @@ impl VerifierMemoryLayout { let at_theta = |words: usize| theta_start + words * WORD_BYTES; let ptr_at_theta = |slot: ThetaSlot| Ptr::memory(at_theta(slot.word())); let theta_mptr = ptr_at_theta(ThetaSlot::Theta); + // P8 (L-5, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): with zero + // user-phase challenges CHALLENGE_MPTR and THETA_MPTR legitimately + // coincide (the challenge window is empty). The hazard is a circuit + // WITH user challenges whose window is under-sized: the transcript + // parser squeezes user challenge j to CHALLENGE_MPTR + 32*j, and the + // very next squeeze unconditionally targets THETA_MPTR -- an overlap + // silently replaces a user challenge with theta while the transcript + // still matches the prover (permanent liveness break, potential + // soundness break). The window is sized by `challenge_indices` while + // the runtime write count is the phase sum of `num_user_challenges`; + // tie the two and pin theta past the window. + { + let user_challenges_total: usize = meta.num_user_challenges.iter().sum(); + assert_eq!( + meta.challenge_indices.len(), + user_challenges_total, + "user-phase challenge window is sized for {} challenge slot(s) but the \ + transcript parser squeezes {user_challenges_total}; the excess would \ + collide with the theta slots", + meta.challenge_indices.len(), + ); + assert!( + challenge_start + user_challenges_total * WORD_BYTES <= theta_start, + "user-phase challenge window [{challenge_start:#x}, {:#x}) overlaps the \ + named challenge slots at THETA_MPTR = {theta_start:#x}; theta's squeeze \ + would silently overwrite a user challenge", + challenge_start + user_challenges_total * WORD_BYTES, + ); + } let total_advices: usize = meta.num_user_advices.iter().sum(); let lookup_helper_chunks_total: usize = meta.lookup_chunks.iter().sum(); @@ -695,29 +809,41 @@ impl VerifierMemoryLayout { + meta.num_lookups + meta.num_trashcans; let committed_g1s = non_quotient_g1s + meta.num_quotients; + // The rot_points / x1_powers / q_com / q_eval_set windows are NOT + // transient scratch even though they sit in the theta scratch band. + // They are written during PCS preparation and then read across several + // *later* phases: `x1_powers` feeds both the rolled q_eval fold + // (`PcsQEvalSourceTable`) and the fused final MSM (`PcsFinalMsm`), + // while `rot_points`/`q_eval_set` feed the f_eval interpolation. Because + // `MemoryLifetime::intersects` treats two different `Phase`s as never + // co-live, tagging these as a dedicated phase would make + // `validate()` blind to any overlap between them and the PCS scratch + // that consumes them. Nothing ever reuses these byte ranges, so they + // are `Permanent`: the planner must guarantee they never overlap any + // other live region. let rot_points_mptr = Ptr::memory(arena.alloc_fixed( "rot_points", at_theta(theta_windows.rot_points_word), config.pcs.rot_points_words * WORD_BYTES, - MemoryLifetime::Phase(MemoryPhase::PcsFixed), + MemoryLifetime::Permanent, )); let x1_powers_mptr = Ptr::memory(arena.alloc_fixed( "x1_powers", at_theta(theta_windows.x1_powers_word), config.pcs.x1_powers_words * WORD_BYTES, - MemoryLifetime::Phase(MemoryPhase::PcsFixed), + MemoryLifetime::Permanent, )); let q_com_mptr = Ptr::memory(arena.alloc_fixed( "q_com_fixed_window", at_theta(theta_windows.q_com_word), config.pcs.q_com_words * WORD_BYTES, - MemoryLifetime::Phase(MemoryPhase::PcsFixed), + MemoryLifetime::Permanent, )); let q_eval_set_mptr = Ptr::memory(arena.alloc_fixed( "q_eval_set", at_theta(theta_windows.q_eval_set_word), config.pcs.q_eval_set_words * WORD_BYTES, - MemoryLifetime::Phase(MemoryPhase::PcsFixed), + MemoryLifetime::Permanent, )); let q_eval_cptr_mptr = Ptr::memory(arena.alloc_fixed( "q_eval_cptr_slot", @@ -769,15 +895,38 @@ impl VerifierMemoryLayout { comms_mptr_base.value().as_usize(), commitments_len, selector_len, - MemoryLifetime::Phase(MemoryPhase::PcsFinalMsm), + MemoryLifetime::PhaseSpan { + start: MemoryPhase::QuotientVm, + end: MemoryPhase::PcsFinalMsm, + }, ); - let batch_invert_scratch_mptr = { + // Both Lagrange-phase regions come from ONE allocator so the second + // lands sequentially after the first: a fresh allocator would reset the + // per-phase cursor back to `selector_acc_mptr` and register a same-phase + // overlap, which `MemoryMap::validate` rejects. Order also matters: + // `batch_invert_scratch` is allocated first so its address stays pinned + // to `selector_acc_mptr` (asserted by + // `selector_accumulators_are_live_from_quotient_to_final_msm`). + // + // `lagrange_denoms` holds the batch-inversion input run (denominators, + // then in-place inverses, then Lagrange values). It was historically + // written in place at `X_N_MPTR`, overlaying theta words 27..51 and + // spilling into the PCS fixed windows for large instance counts; as a + // registered region the arena's overlap model enforces disjointness + // structurally instead of by write-ordering coincidence. + let (batch_invert_scratch_mptr, lagrange_denoms_mptr) = { let mut scratch = arena.scratch_allocator(selector_acc_mptr); - scratch.alloc_phase_scratch( + let batch_invert_scratch_mptr = scratch.alloc_phase_scratch( "batch_invert_scratch", batch_invert_len, MemoryPhase::LagrangeBatchInvert, - ) + ); + let lagrange_denoms_mptr = scratch.alloc_phase_scratch( + "lagrange_denoms", + lagrange_denoms_len, + MemoryPhase::LagrangeBatchInvert, + ); + (batch_invert_scratch_mptr, lagrange_denoms_mptr) }; let quotient_tmp_base = (selector_acc_mptr + selector_len).next_multiple_of(WORD_BYTES); let (quotient_tmp_mptr, quotient_stack_mptr) = { @@ -794,6 +943,29 @@ impl VerifierMemoryLayout { ); (quotient_tmp_mptr, quotient_stack_mptr) }; + // Trace renders expand the linearization terms into their own G1MSM + // frame at `SELECTOR_ACC_MPTR + selector_len`, i.e. starting exactly at + // `quotient_tmp_base`. Register it so the write band is visible to the + // arena and to the trace-log-word placement below; without this the + // only thing keeping it in bounds is the incidental fact that the PCS + // scratch allocated from the same base happens to be at least as long. + let linearization_trace_msm_mptr = { + let mut scratch = arena.scratch_allocator(quotient_tmp_base); + scratch.alloc_phase_scratch( + "linearization_trace_msm", + lin_trace_len, + MemoryPhase::LinearizationTrace, + ) + }; + // QuotientAndLinearization.yul derives the frame base as + // `add(SELECTOR_ACC_MPTR, selector_len)`. Pin the equality so the + // registered region cannot drift away from the address the template + // actually writes. + assert_eq!( + linearization_trace_msm_mptr, + selector_acc_mptr + selector_len, + "linearization trace MSM region must start where the template computes lin_scratch" + ); let pcs_scratch_mptr = quotient_tmp_mptr; let ( pcs_q_eval_source_table_mptr, @@ -844,12 +1016,19 @@ impl VerifierMemoryLayout { vk_start + vk.len(), challenge_start + meta.challenge_indices.len() * WORD_BYTES, theta_start + theta_windows.rot_points_word * WORD_BYTES, + rot_points_mptr.value().as_usize() + config.pcs.rot_points_words * WORD_BYTES, + x1_powers_mptr.value().as_usize() + config.pcs.x1_powers_words * WORD_BYTES, + q_com_mptr.value().as_usize() + config.pcs.q_com_words * WORD_BYTES, + q_eval_set_mptr.value().as_usize() + config.pcs.q_eval_set_words * WORD_BYTES, + q_eval_cptr_mptr.value().as_usize() + WORD_BYTES, g1_identity_mptr.value().as_usize() + G1_BYTES, reversed_evals_mptr.value().as_usize() + meta.num_evals * WORD_BYTES, comms_mptr_base.value().as_usize() + commitments_len, selector_acc_mptr + selector_len, batch_invert_scratch_mptr + batch_invert_len, + lagrange_denoms_mptr + lagrange_denoms_len, quotient_stack_mptr + quotient_stack_len.max(MODEXP_FRAME_BYTES), + linearization_trace_msm_mptr + lin_trace_len, pcs_q_eval_source_table_mptr + q_eval_source_len, pcs_q_com_trace_scratch_mptr + q_com_trace_len, pcs_final_msm_scratch_mptr + final_msm_len, @@ -925,8 +1104,10 @@ impl VerifierMemoryLayout { quotient_limb_comms_mptr_base, selector_acc_mptr, batch_invert_scratch_mptr, + lagrange_denoms_mptr, quotient_tmp_mptr, quotient_stack_mptr, + quotient_stack_hi: quotient_stack_mptr + quotient_stack_len.max(MODEXP_FRAME_BYTES), pcs_q_eval_source_table_mptr, pcs_q_com_trace_scratch_mptr, pcs_final_msm_scratch_mptr, @@ -985,6 +1166,24 @@ impl VerifierMemoryLayout { } } + // The verifier body runs inside `assembly ("memory-safe")`, so solc's + // via-IR stack-to-memory mover reserves spill slots upward from 0x80. + // A generated region below `LOW_MEMORY_SCRATCH_START` could share + // bytes with a live spill slot, and the lifetime model cannot see + // solc's opaque spill liveness -- so enforce disjointness by address. + // `compiled_memoryguard_does_not_overlap_generated_layout` checks the + // complementary bound, `reserved_end <= LOW_MEMORY_SCRATCH_START`, + // against real compiled verifier and quotient-evaluator bytecode. + for region in &self.map.regions { + if region.len != 0 && region.start < LOW_MEMORY_SCRATCH_START { + return Err(format!( + "memory region {} starts at {:#x}, below LOW_MEMORY_SCRATCH_START ({:#x}); \ + it can overlap solc's via-IR stack-to-memory spill window [0x80, reserved_end)", + region.name, region.start, LOW_MEMORY_SCRATCH_START + )); + } + } + let expected_scalar_inv = self.vk_mptr.value().as_usize().saturating_sub(MODEXP_SCRATCH_BYTES); if self.scalar_inv_scratch_mptr != expected_scalar_inv { @@ -1020,6 +1219,9 @@ impl VerifierMemoryLayout { )); } + // The Lagrange batch-inversion input run lives in the registered + // `lagrange_denoms` phase region, so `map.validate()` below covers its + // disjointness structurally; no separate capacity check is needed. self.map.validate()?; Ok(()) @@ -1037,25 +1239,31 @@ pub(crate) fn commitment_g1_count(meta: &ConstraintSystemMeta) -> usize { + meta.num_quotients } +/// Number of Fr words the generated Lagrange block writes into the +/// `lagrange_denoms` region as the batch-inversion input run. +/// +/// The input range covers: +/// - num_instances public Lagrange denominators, or one fallback word when +/// there are no public instances; +/// - `abs(rotation_last)` negative-row denominators; +/// - x_n - 1. +pub(crate) fn batch_invert_input_words(meta: &ConstraintSystemMeta, num_instances: usize) -> usize { + if num_instances == 0 { + meta.rotation_last.unsigned_abs() as usize + 2 + } else { + num_instances + meta.rotation_last.unsigned_abs() as usize + 1 + } +} + /// Scratch size required by the batched scalar-inversion helper. fn batch_invert_scratch_bytes(meta: &ConstraintSystemMeta, num_instances: usize) -> usize { // The template calls: // batch_invert(X_N_MPTR, mptr_end + WORD_BYTES, scratch, r) // - // The input range covers: - // - num_instances public Lagrange denominators, or one fallback word when - // there are no public instances; - // - `abs(rotation_last)` negative-row denominators; - // - x_n - 1. - // // For N inputs, the batched inversion stores N-2 prefix products and then // overlays one modexp frame at the current prefix pointer. Singletons use // only the frame. - let input_words = if num_instances == 0 { - meta.rotation_last.unsigned_abs() as usize + 2 - } else { - num_instances + meta.rotation_last.unsigned_abs() as usize + 1 - }; + let input_words = batch_invert_input_words(meta, num_instances); MODEXP_FRAME_BYTES + input_words.saturating_sub(2) * WORD_BYTES } @@ -1101,6 +1309,25 @@ mod tests { map.validate().expect("disjoint scratch lifetimes"); } + #[test] + fn phase_spans_cover_each_phase_in_their_range() { + let lifetime = MemoryLifetime::PhaseSpan { + start: MemoryPhase::QuotientVm, + end: MemoryPhase::PcsFinalMsm, + }; + + for phase in [ + MemoryPhase::QuotientVm, + MemoryPhase::PcsQEvalSourceTable, + MemoryPhase::PcsQComTrace, + MemoryPhase::PcsFinalMsm, + ] { + assert!(lifetime.intersects(&MemoryLifetime::Phase(phase))); + } + assert!(!lifetime.intersects(&MemoryLifetime::Phase(MemoryPhase::LagrangeBatchInvert))); + assert!(!lifetime.intersects(&MemoryLifetime::Phase(MemoryPhase::PcsPairing))); + } + #[test] fn unaligned_regions_fail() { let mut map = MemoryMap::default(); @@ -1236,7 +1463,7 @@ mod tests { let layout = VerifierMemoryLayout::new( &meta, &vk, - Ptr::memory(0x1000), + Ptr::memory(0x2000), VerifierMemoryLayoutConfig::default(), ); let theta = layout.theta_mptr.value().as_usize(); @@ -1281,6 +1508,114 @@ mod tests { ); } + /// P8 (L-5): a circuit with user-phase challenges must get a challenge + /// window that ends at or before THETA_MPTR, or theta's unconditional + /// squeeze silently overwrites user challenge 0. + #[test] + fn user_phase_challenge_window_precedes_theta() { + let meta = ConstraintSystemMeta { + num_user_advices: vec![1, 1], + num_user_challenges: vec![1, 2], + challenge_indices: vec![0, 1, 2], + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let layout = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x2000), + VerifierMemoryLayoutConfig::default(), + ); + let challenge = layout.challenge_mptr.value().as_usize(); + let theta = layout.theta_mptr.value().as_usize(); + assert!( + challenge + 3 * WORD_BYTES <= theta, + "user challenge window [{challenge:#x}, {:#x}) must end before \ + THETA_MPTR = {theta:#x}", + challenge + 3 * WORD_BYTES + ); + // With zero user challenges the two legitimately coincide. + let empty_layout = VerifierMemoryLayout::new( + &ConstraintSystemMeta::default(), + &vk, + Ptr::memory(0x2000), + VerifierMemoryLayoutConfig::default(), + ); + assert_eq!( + empty_layout.challenge_mptr.value().as_usize(), + empty_layout.theta_mptr.value().as_usize(), + ); + } + + /// P8 (L-5): a window sized for fewer slots than the transcript parser + /// squeezes must refuse to plan. + #[test] + #[should_panic(expected = "user-phase challenge window is sized for")] + fn undersized_user_challenge_window_is_rejected() { + let meta = ConstraintSystemMeta { + num_user_advices: vec![1], + num_user_challenges: vec![2], + challenge_indices: vec![0], + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let _ = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x2000), + VerifierMemoryLayoutConfig::default(), + ); + } + + /// P9 (L-7): across instance-count and rotation shapes, the + /// batch-inversion scratch must hold exactly the run the Lagrange block + /// writes, and the denominator region must match the input count -- the + /// two regions are adjacent with zero slack, and an overflow corrupts + /// denominator[0] without reverting. + #[test] + fn batch_invert_scratch_capacity_matches_lagrange_run() { + let vk = synthetic_vk(); + for (num_instances, rotation_last) in + [(0usize, -1i32), (1, -1), (2, -3), (19, -6), (200, -6)] + { + let meta = ConstraintSystemMeta { + rotation_last, + ..ConstraintSystemMeta::default() + }; + let layout = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x2000), + VerifierMemoryLayoutConfig { + num_instances, + ..VerifierMemoryLayoutConfig::default() + }, + ); + let n = batch_invert_input_words(&meta, num_instances); + let scratch = layout + .map + .region("batch_invert_scratch") + .expect("batch_invert_scratch region is registered"); + let denoms = layout + .map + .region("lagrange_denoms") + .expect("lagrange_denoms region is registered"); + assert_eq!(denoms.len, n * WORD_BYTES); + assert_eq!( + scratch.len, + n.saturating_sub(2) * WORD_BYTES + MODEXP_FRAME_BYTES, + "scratch must hold n-2 prefix products plus one modexp frame \ + for n = {n} (num_instances = {num_instances})" + ); + assert_eq!( + scratch.start + scratch.len, + denoms.start, + "regions are adjacent by construction; the capacity assert is \ + what keeps the adjacency safe" + ); + } + } + #[test] fn fixed_low_memory_regions_are_planner_registered() { let meta = ConstraintSystemMeta { @@ -1291,7 +1626,7 @@ mod tests { let layout = VerifierMemoryLayout::new( &meta, &vk, - Ptr::memory(0x1000), + Ptr::memory(0x2000), VerifierMemoryLayoutConfig::default(), ); @@ -1364,13 +1699,36 @@ mod tests { meta.num_simple_selectors * G1_MSM_PAIR_BYTES ); + // These two regions carry different phases, and `MemoryLifetime:: + // intersects` treats distinct phases as never co-live -- so the arena's + // own overlap validation is structurally blind to an overlap here and + // this assertion is the only thing that catches one. + let batch = layout + .map + .region("accumulator_pairing_batch") + .expect("accumulator pairing batch registered"); + let final_pairing = layout + .map + .region("final_pairing_scratch") + .expect("final pairing scratch registered"); + assert!( + batch.start + batch.len <= final_pairing.start, + "accumulator pairing batch [{:#x}, {:#x}) must not overlap final pairing scratch \ + [{:#x}, {:#x}): the last word of the hashed ACC_LHS copy would share bytes with \ + ec_pairing's input frame", + batch.start, + batch.start + batch.len, + final_pairing.start, + final_pairing.start + final_pairing.len, + ); + let scalar_inv = layout .map .region("scalar_inv_scratch") .expect("scalar inversion scratch registered"); assert_eq!( layout.scalar_inv_scratch_mptr, - 0x1000 - MODEXP_SCRATCH_BYTES + 0x2000 - MODEXP_SCRATCH_BYTES ); assert_eq!(scalar_inv.start, layout.scalar_inv_scratch_mptr); assert_eq!(scalar_inv.len, MODEXP_FRAME_BYTES); @@ -1384,17 +1742,17 @@ mod tests { let windows = ThetaWindowLayout::compatibility(); let mut config = VerifierMemoryLayoutConfig::default(); config.pcs.rot_points_words = windows.rot_points_cap_words + 1; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); assert!(layout.validate().unwrap_err().contains("ROT_POINTS_MPTR")); let mut config = VerifierMemoryLayoutConfig::default(); config.pcs.x1_powers_words = windows.x1_powers_cap_words + 1; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); assert!(layout.validate().unwrap_err().contains("X1_POWERS_MPTR")); let mut config = VerifierMemoryLayoutConfig::default(); config.pcs.q_eval_set_words = windows.q_eval_set_cap_words + 1; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); assert!(layout.validate().unwrap_err().contains("Q_EVAL_SET_MPTR")); } @@ -1406,13 +1764,42 @@ mod tests { acc_msm_terms: 4, ..VerifierMemoryLayoutConfig::default() }; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); let region = layout.map.region("accumulator_msm").expect("accumulator MSM region registered"); assert_eq!(region.len, 4 * G1_MSM_PAIR_BYTES); } + #[test] + fn selector_accumulators_are_live_from_quotient_to_final_msm() { + let meta = ConstraintSystemMeta { + num_simple_selectors: 1, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let layout = VerifierMemoryLayout::new( + &meta, + &vk, + Ptr::memory(0x2000), + VerifierMemoryLayoutConfig::default(), + ); + let selector = layout + .map + .region("selector_accumulators") + .expect("selector accumulators registered"); + + assert_eq!( + selector.lifetime, + MemoryLifetime::PhaseSpan { + start: MemoryPhase::QuotientVm, + end: MemoryPhase::PcsFinalMsm, + } + ); + assert_eq!(layout.batch_invert_scratch_mptr, layout.selector_acc_mptr); + layout.validate().expect("earlier batch inversion may reuse selector bytes"); + } + #[test] fn trace_log_word_is_registered_after_scratch_regions() { let meta = ConstraintSystemMeta { @@ -1431,7 +1818,7 @@ mod tests { }, ..VerifierMemoryLayoutConfig::default() }; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); let region = layout.map.region("trace_u256_log_word").expect("trace_u256 region registered"); @@ -1443,6 +1830,52 @@ mod tests { layout.validate().expect("trace log word must not overlap"); } + #[test] + fn trace_log_word_accounts_for_theta_window_region_ends() { + let meta = ConstraintSystemMeta::default(); + let vk = synthetic_vk(); + let window_words = 4096; + let config = VerifierMemoryLayoutConfig { + pcs: PcsMemoryRequirements { + rot_points_words: window_words, + x1_powers_words: window_words, + q_com_words: window_words, + q_eval_set_words: window_words, + ..PcsMemoryRequirements::default() + }, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); + + for (name, end) in [ + ( + "rot_points", + layout.rot_points_mptr.value().as_usize() + window_words * WORD_BYTES, + ), + ( + "x1_powers", + layout.x1_powers_mptr.value().as_usize() + window_words * WORD_BYTES, + ), + ( + "q_com_fixed_window", + layout.q_com_mptr.value().as_usize() + window_words * WORD_BYTES, + ), + ( + "q_eval_set", + layout.q_eval_set_mptr.value().as_usize() + window_words * WORD_BYTES, + ), + ( + "q_eval_cptr_slot", + layout.q_eval_cptr_mptr.value().as_usize() + WORD_BYTES, + ), + ] { + assert!( + layout.trace_u256_mptr >= end, + "trace log word should be after {name}" + ); + } + } + #[test] fn batch_invert_scratch_region_tracks_instance_shape() { let meta = ConstraintSystemMeta { @@ -1454,7 +1887,7 @@ mod tests { num_instances: 5, ..VerifierMemoryLayoutConfig::default() }; - let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x1000), config); + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); let region = layout .map .region("batch_invert_scratch") @@ -1465,4 +1898,79 @@ mod tests { MODEXP_FRAME_BYTES + (5 + 3 + 1 - 2) * WORD_BYTES ); } + + #[test] + fn lagrange_denoms_region_tracks_instance_shape() { + let meta = ConstraintSystemMeta { + rotation_last: -3, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let config = VerifierMemoryLayoutConfig { + num_instances: 5, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); + let region = layout + .map + .region("lagrange_denoms") + .expect("lagrange denominator region registered"); + + // num_instances + |rotation_last| denominators plus the trailing + // `x_n - 1` word. + assert_eq!(region.len, (5 + 3 + 1) * WORD_BYTES); + assert_eq!( + region.lifetime, + MemoryLifetime::Phase(MemoryPhase::LagrangeBatchInvert) + ); + assert_eq!(layout.lagrange_denoms_mptr, region.start); + // Sequential same-phase allocation: the run sits directly above the + // prefix-product scratch, which itself stays pinned to the selector + // accumulator base. + let scratch = layout + .map + .region("batch_invert_scratch") + .expect("batch invert scratch region registered"); + assert_eq!(region.start, scratch.start + scratch.len); + layout.validate().expect("layout with registered denominator run is valid"); + + // Zero public instances still needs one denominator slot for L_0 + // recovery: |rotation_last| + 2 words total. + let config = VerifierMemoryLayoutConfig { + num_instances: 0, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); + let region = layout + .map + .region("lagrange_denoms") + .expect("lagrange denominator region registered"); + assert_eq!(region.len, (3 + 2) * WORD_BYTES); + layout.validate().expect("zero-instance layout is valid"); + } + + /// The registered denominator region removes the historical instance-count + /// cliff: a count far beyond the old 175-word live-memory cap now simply + /// grows the region, and the arena still validates. + #[test] + fn lagrange_denoms_region_scales_beyond_the_old_live_memory_cliff() { + let meta = ConstraintSystemMeta { + rotation_last: -3, + ..ConstraintSystemMeta::default() + }; + let vk = synthetic_vk(); + let config = VerifierMemoryLayoutConfig { + num_instances: 500, + ..VerifierMemoryLayoutConfig::default() + }; + let layout = VerifierMemoryLayout::new(&meta, &vk, Ptr::memory(0x2000), config); + let region = layout + .map + .region("lagrange_denoms") + .expect("lagrange denominator region registered"); + assert_eq!(region.len, (500 + 3 + 1) * WORD_BYTES); + layout + .validate() + .expect("large instance counts are structurally valid with a registered run"); + } } diff --git a/proofs/solidity-verifier/src/lowering/layout/mod.rs b/proofs/solidity-verifier/src/lowering/layout/mod.rs index 1c885ea67..e574366a1 100644 --- a/proofs/solidity-verifier/src/lowering/layout/mod.rs +++ b/proofs/solidity-verifier/src/lowering/layout/mod.rs @@ -26,7 +26,22 @@ pub(crate) const SOLIDITY_ALLOCATABLE_MEMORY_START: usize = 0x80; /// The full reserved prefix: scratch, free-memory pointer, and zero slot. pub(crate) const SOLIDITY_RESERVED_MEMORY_BYTES: usize = SOLIDITY_ALLOCATABLE_MEMORY_START; /// Generated verifier transcript and low-memory precompile scratch base. -pub(crate) const LOW_MEMORY_SCRATCH_START: usize = SOLIDITY_ALLOCATABLE_MEMORY_START; +/// +/// Deliberately *above* [`SOLIDITY_ALLOCATABLE_MEMORY_START`]. The verifier +/// body is wrapped in `assembly ("memory-safe")`, which is what lets solc's +/// via-IR stack-to-memory mover run at all -- without it the block does not +/// compile (stack too deep). That mover reserves spill slots from `0x80` +/// upward and records the top in the runtime's `mstore(0x40, ...)` prologue. +/// Observed reservations run from `0x80` (none) to `0x8e0`, varying with the +/// circuit, the solc release, and the optimizer schedule. +/// +/// Basing the generated layout at `0x80` therefore put solc's spill slots and +/// the verifier's own transcript buffer in the same bytes, kept apart only by +/// live ranges that nothing enforced. Starting above the largest observed +/// reservation makes them disjoint by construction; +/// `compiled_memoryguard_does_not_overlap_generated_layout` fails the build if +/// a future circuit or compiler pushes the reservation past this base. +pub(crate) const LOW_MEMORY_SCRATCH_START: usize = 0x1000; /// Start of the Keccak transcript buffer used by the assembly helpers. pub(crate) const TRANSCRIPT_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; /// Shared low-memory scratch for PCS pairing serialization. @@ -36,7 +51,12 @@ pub(crate) const VERIFIER_RETURN_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; /// Return buffer used by split quotient evaluator calls. pub(crate) const QUOTIENT_RETURN_BUFFER_START: usize = LOW_MEMORY_SCRATCH_START; /// Constructor-time memory base for the separate VK runtime payload. -pub(crate) const VK_CONSTRUCTOR_PAYLOAD_START: usize = LOW_MEMORY_SCRATCH_START; +/// +/// Deliberately *not* derived from [`LOW_MEMORY_SCRATCH_START`]. This buffer +/// belongs to `Halo2VerifyingKey`, a separate contract whose assembly is not +/// annotated `memory-safe`, so solc reserves no via-IR spill window there and +/// the payload can sit at Solidity's normal allocatable start. +pub(crate) const VK_CONSTRUCTOR_PAYLOAD_START: usize = SOLIDITY_ALLOCATABLE_MEMORY_START; /// Number of EVM words in one Fr scalar. pub(crate) const FR_WORDS: usize = 1; /// EIP-2537 padded G1 encoding: x_hi, x_lo, y_hi, y_lo. @@ -89,7 +109,17 @@ pub(crate) const PCS_STATIC_WORKING_WORDS: usize = 32; /// Static two-pair KZG pairing scratch plus one return word. pub(crate) const PAIRING_STATIC_WORKING_WORDS: usize = PAIRING_TWO_PAIR_BYTES / WORD_BYTES + 1; /// Low-memory frame used by the final two-pair KZG pairing helper. -pub(crate) const FINAL_PAIRING_SCRATCH_START: usize = PAIRING_TWO_PAIR_BYTES; +/// +/// Placed past the end of the accumulator pairing-batch hash frame, which +/// occupies `[PAIRING_BATCH_PTR, PAIRING_BATCH_PTR + PAIRING_BATCH_HASH_BYTES)` +/// = `[0x1000, 0x1240)` (tag + vk_digest + four G1 points since audit I-7). +/// Starting lower would put the last word of the hashed ACC_LHS copy inside +/// this scratch. +/// The two regions carry different `MemoryPhase`s, and `MemoryLifetime:: +/// intersects` treats distinct phases as never co-live, so the planner cannot +/// catch that overlap -- it has to be avoided by construction here. +pub(crate) const FINAL_PAIRING_SCRATCH_START: usize = + accumulator::PAIRING_BATCH_PTR + accumulator::PAIRING_BATCH_HASH_BYTES; pub(crate) mod precompile { //! EVM precompile addresses used by the generated verifier. These values @@ -107,6 +137,136 @@ pub(crate) mod precompile { pub(crate) const PAIRING_ADDRESS: usize = 0x0f; } +pub(crate) mod gas { + //! Exact gas schedule for the precompiles the generated verifier calls, + //! from EIP-2537 (BLS12-381) and EIP-2565/EIP-7883 (modexp). + //! + //! A failing EIP-2537 or modexp call consumes ALL gas supplied to the + //! `STATICCALL`, so every generated call site forwards the exact scheduled + //! cost instead of `gas()`: an attacker-supplied malformed point can then + //! burn at most the scheduled cost of the single failing call rather than + //! 63/64 of the transaction budget (measured 29.5M of a 30M limit before + //! this bound; see docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md, M-2). + //! + //! Sufficiency: EIP-2537's "DDoS protection" rationale guarantees the + //! schedule prices the worst case, so forwarding exactly the scheduled + //! amount succeeds by construction on any conformant implementation of + //! the current schedule. + //! + //! Multi-schedule bounds: where more than one schedule is live across the + //! chains this verifier targets, the bound is the MAXIMUM over those + //! schedules rather than the one for a single fork (see + //! [`modexp_gas_word_frame`]). Over-forwarding costs nothing on success -- + //! unused gas is returned -- and only widens the burn of one *failing* + //! call by the difference, whereas under-forwarding bricks the verifier. + //! + //! Liveness caveat: if a future fork reprices these precompiles above the + //! bounds rendered here, deployed verifiers start reverting on valid + //! proofs and must be regenerated and redeployed. The constructor smoke + //! probes forward the same bounds for every precompile the runtime + //! depends on -- EIP-2537 *and* modexp (MF-1) -- so deployment onto an + //! already-repriced chain fails fast instead of bricking at proof time. + + /// EIP-2537 G1ADD flat cost. + pub(crate) const G1ADD_GAS: u64 = 375; + /// EIP-2537 G1 per-pair base multiplication cost. + const G1_MSM_MULTIPLICATION_COST: u64 = 12_000; + /// EIP-2537 MSM discount denominator. + const MSM_MULTIPLIER: u64 = 1_000; + /// EIP-2537 G1 MSM discount table for k = 1..=128 pairs, in parts per + /// [`MSM_MULTIPLIER`]. Entry `[k - 1]` is the discount for `k` pairs; + /// `k > 128` uses the final entry (`max_discount = 519`). + const G1_MSM_DISCOUNT: [u64; 128] = [ + 1000, 949, 848, 797, 764, 750, 738, 728, 719, 712, 705, 698, 692, 687, 682, 677, 673, 669, + 665, 661, 658, 654, 651, 648, 645, 642, 640, 637, 635, 632, 630, 627, 625, 623, 621, 619, + 617, 615, 613, 611, 609, 608, 606, 604, 603, 601, 599, 598, 596, 595, 593, 592, 591, 589, + 588, 586, 585, 584, 582, 581, 580, 579, 577, 576, 575, 574, 573, 572, 570, 569, 568, 567, + 566, 565, 564, 563, 562, 561, 560, 559, 558, 557, 556, 555, 554, 553, 552, 551, 550, 549, + 548, 547, 547, 546, 545, 544, 543, 542, 541, 540, 540, 539, 538, 537, 536, 536, 535, 534, + 533, 532, 532, 531, 530, 529, 528, 528, 527, 526, 525, 525, 524, 523, 522, 522, 521, 520, + 520, 519, + ]; + + /// EIP-2537 G1MSM cost for `k` (point, scalar) pairs: + /// `(k * 12000 * discount(k)) // 1000`. + pub(crate) fn g1msm_gas(pairs: usize) -> u64 { + assert!(pairs > 0, "G1MSM gas bound requested for an empty MSM"); + let discount = if pairs <= G1_MSM_DISCOUNT.len() { + G1_MSM_DISCOUNT[pairs - 1] + } else { + G1_MSM_DISCOUNT[G1_MSM_DISCOUNT.len() - 1] + }; + (pairs as u64) * G1_MSM_MULTIPLICATION_COST * discount / MSM_MULTIPLIER + } + + /// EIP-2537 pairing cost for `k` (G1, G2) pairs: `32600*k + 37700`. + pub(crate) const fn pairing_gas(pairs: u64) -> u64 { + 32_600 * pairs + 37_700 + } + + /// Modexp bound for the only frame shape the verifier emits: 32-byte + /// base, 32-byte exponent, 32-byte modulus. + /// + /// Two schedules are live across the chains this verifier targets, so the + /// rendered bound is the maximum of both: + /// + /// * **EIP-2565** (Berlin): `max(200, multiplication_complexity * + /// iteration_count / 3)`. With `words = ceil(32/8) = 4` and + /// `multiplication_complexity = words^2 = 16`, that is + /// `max(200, 16 * 255 / 3) = 1360`. + /// * **EIP-7883** (Osaka/Fusaka): the `/ 3` divisor is **removed** and the + /// floor is raised to 500, giving `max(500, 16 * 255) = 4080`. + /// + /// MF-1: this function previously returned only the EIP-2565 value and + /// asserted that EIP-7883 "only reprices operands wider than 32 bytes". + /// That reading was wrong. EIP-7883 changes two independent things: the + /// `multiplication_complexity` branch for `max_length > 32` (`2 * words^2`, + /// which indeed does not apply here), *and* the removal of the `/ 3` + /// divisor from the final `multiplication_complexity * iteration_count` + /// product, which applies to every operand size. A verifier rendered with + /// the 1360 bound deploys fine on a repriced chain and then reverts + /// `PrecompileFailed` on every proof, because `staticcall` forwards a + /// fixed amount and the precompile runs out of gas inside the mandatory + /// Lagrange batch inversion. + /// + /// `iteration_count` is the generic upper bound for any 32-byte exponent + /// (`exponent.bit_length() - 1 <= 255`). Both exponents the verifier + /// actually emits are `FR_MODULUS - 2`, whose 255-bit length gives 254 + /// iterations and an exact EIP-7883 price of 4064; the extra 16 gas keeps + /// the bound valid for any 32-byte exponent a future emitter might use. + pub(crate) const fn modexp_gas_word_frame() -> u64 { + const WORDS: u64 = 4; + const MULTIPLICATION_COMPLEXITY: u64 = WORDS * WORDS; + // Upper bound over every 32-byte exponent: `bit_length() - 1 <= 255`. + const MAX_ITERATION_COUNT: u64 = 255; + + // EIP-2565: divisor 3, floor 200. + let eip2565 = { + let cost = MULTIPLICATION_COMPLEXITY * MAX_ITERATION_COUNT / 3; + if cost < 200 { + 200 + } else { + cost + } + }; + // EIP-7883: no divisor, floor 500. + let eip7883 = { + let cost = MULTIPLICATION_COMPLEXITY * MAX_ITERATION_COUNT; + if cost < 500 { + 500 + } else { + cost + } + }; + + if eip2565 > eip7883 { + eip2565 + } else { + eip7883 + } + } +} + pub(crate) mod modexp_frame { //! 32-byte base/exponent/modulus EIP-198 frame offsets. @@ -141,20 +301,48 @@ pub(crate) mod accumulator { pub(crate) const CARRIED_SCALARS: usize = 2; /// Low-memory hash frame for batching the accumulator pairing with KZG: /// domain tag word, KZG rhs/lhs G1s, then accumulator rhs/lhs G1s. - pub(crate) const PAIRING_BATCH_PTR: usize = 0x100; - /// ASCII `"pairing-batch-acc-kzg"` right-padded to one EVM word. + /// + /// Rooted at [`super::LOW_MEMORY_SCRATCH_START`] like every other + /// low-memory scratch base: the frame historically sat at `0x100`, inside + /// the `[0x80, reserved_end)` window solc's via-IR stack-to-memory mover + /// reserves for spill slots, so a live spill could silently corrupt the + /// alpha Fiat-Shamir preimage or the pairing inputs built here. + pub(crate) const PAIRING_BATCH_PTR: usize = super::LOW_MEMORY_SCRATCH_START; + /// Domain-separation word for the accumulator pairing batch. + /// + /// This is a 29-byte numeric literal: ASCII `"pairing-batch-acc-kzg"` + /// (21 bytes) followed by 8 zero bytes. FinalPairing.yul stores it with + /// `mstore`, which left-pads numeric literals to a full word, so the word + /// actually hashed into alpha is + /// + /// ```text + /// 00 00 00 || "pairing-batch-acc-kzg" || 00 * 8 + /// ``` + /// + /// i.e. NOT right-padded ASCII, as this comment previously claimed. The + /// value is still a fixed unique constant, so domain separation is + /// unaffected -- but any reimplementation or differential fixture that + /// derives alpha from the right-padded form will disagree with the + /// deployed verifier on accept/reject. pub(crate) const PAIRING_BATCH_DOMAIN_TAG_HEX: &str = "0x70616972696e672d62617463682d6163632d6b7a670000000000000000"; + /// VK digest offset inside the batch hash frame (I-7, + /// docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): alpha was previously a + /// function of the four G1 points alone, binding the verifying key only + /// transitively. Absorbing `vk_digest` makes the binding local at zero + /// marginal cost -- alpha is verifier-local batching randomness, so no + /// prover interaction changes. + pub(crate) const PAIRING_BATCH_VK_DIGEST_OFFSET: usize = WORD_BYTES; /// KZG pairing RHS point offset inside the batch hash frame. - pub(crate) const PAIRING_BATCH_RHS_OFFSET: usize = WORD_BYTES; + pub(crate) const PAIRING_BATCH_RHS_OFFSET: usize = 2 * WORD_BYTES; /// KZG pairing LHS point offset inside the batch hash frame. - pub(crate) const PAIRING_BATCH_LHS_OFFSET: usize = WORD_BYTES + G1_BYTES; + pub(crate) const PAIRING_BATCH_LHS_OFFSET: usize = 2 * WORD_BYTES + G1_BYTES; /// Accumulator RHS point offset inside the batch hash frame. - pub(crate) const PAIRING_BATCH_ACC_RHS_OFFSET: usize = WORD_BYTES + 2 * G1_BYTES; + pub(crate) const PAIRING_BATCH_ACC_RHS_OFFSET: usize = 2 * WORD_BYTES + 2 * G1_BYTES; /// Accumulator LHS point offset inside the batch hash frame. - pub(crate) const PAIRING_BATCH_ACC_LHS_OFFSET: usize = WORD_BYTES + 3 * G1_BYTES; + pub(crate) const PAIRING_BATCH_ACC_LHS_OFFSET: usize = 2 * WORD_BYTES + 3 * G1_BYTES; /// Number of frame bytes absorbed into the accumulator/KZG batch challenge. - pub(crate) const PAIRING_BATCH_HASH_BYTES: usize = WORD_BYTES + 4 * G1_BYTES; + pub(crate) const PAIRING_BATCH_HASH_BYTES: usize = 2 * WORD_BYTES + 4 * G1_BYTES; } pub(crate) mod quotient_limb { @@ -591,6 +779,7 @@ pub(crate) mod trace { pub(crate) const PROOF_COMMIT_BASE: usize = 10_000; // Proof G1 reads. pub(crate) const PROOF_EVAL_BASE: usize = 20_000; // Proof scalar eval reads. pub(crate) const QUOTIENT_IDENTITY_BASE: u64 = 30_000; // Quotient identities. + pub(crate) const PCS_Q_COM_BASE: u64 = 40_000; // PCS q_com points. pub(crate) const PCS_SERIALIZED_POINT_SET_BASE: u64 = 41_000; // PCS point sets. pub(crate) const SELECTOR_FOLD_BASE: usize = 60_000; // Selector accumulators. } @@ -618,7 +807,14 @@ mod tests { assert_eq!(super::SOLIDITY_FREE_MEMORY_POINTER_SLOT, 0x40); assert_eq!(super::SOLIDITY_ZERO_SLOT, 0x60); assert_eq!(super::SOLIDITY_RESERVED_MEMORY_BYTES, 0x80); - assert_eq!(super::TRANSCRIPT_BUFFER_START, 0x80); + assert_eq!( + super::TRANSCRIPT_BUFFER_START, + super::LOW_MEMORY_SCRATCH_START + ); + // Above Solidity's allocatable start on purpose: solc reserves + // via-IR spill slots upward from 0x80 in this contract. + const _: () = + assert!(super::LOW_MEMORY_SCRATCH_START > super::SOLIDITY_ALLOCATABLE_MEMORY_START); assert_eq!(super::VK_CONSTRUCTOR_PAYLOAD_START, 0x80); } @@ -680,6 +876,7 @@ mod tests { assert_eq!(trace::PROOF_COMMIT_BASE, 10_000); assert_eq!(trace::PROOF_EVAL_BASE, 20_000); assert_eq!(trace::QUOTIENT_IDENTITY_BASE, 30_000); + assert_eq!(trace::PCS_Q_COM_BASE, 40_000); assert_eq!(trace::PCS_SERIALIZED_POINT_SET_BASE, 41_000); assert_eq!(trace::SELECTOR_FOLD_BASE, 60_000); } @@ -703,8 +900,18 @@ mod tests { assert_eq!(accumulator::LIMB_BITS, 56); assert_eq!(accumulator::LIMBS, 7); assert_eq!(accumulator::LIMBS_PER_WORD, 4); - assert_eq!(accumulator::PAIRING_BATCH_PTR, 0x100); - assert_eq!(accumulator::PAIRING_BATCH_HASH_BYTES, 0x220); + // Above solc's via-IR spill window like every other low-memory base. + assert_eq!( + accumulator::PAIRING_BATCH_PTR, + super::LOW_MEMORY_SCRATCH_START + ); + // One word longer since the vk_digest joined the alpha preimage + // (audit I-7): tag + vk_digest + four G1 points. + assert_eq!(accumulator::PAIRING_BATCH_HASH_BYTES, 0x240); + assert_eq!( + super::FINAL_PAIRING_SCRATCH_START, + super::LOW_MEMORY_SCRATCH_START + 0x240 + ); assert_eq!(quotient_limb::LIMBS, 7); assert_eq!(quotient_limb::PAIRWISE_TERMS, 49); assert_eq!(quotient_limb::PAIRWISE_COEFFS, 13); diff --git a/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs b/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs index c7c21f047..a416d3576 100644 --- a/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs +++ b/proofs/solidity-verifier/src/lowering/layout/vk_payload.rs @@ -111,7 +111,9 @@ impl VkPayloadLayout { word_offset: self.cursor_words, word_len, }; - self.cursor_words += word_len; + self.cursor_words = self.cursor_words.checked_add(word_len).ok_or_else(|| { + format!("VK payload word cursor overflow reserving {kind:?} ({word_len} words)") + })?; self.sections.push(section); Ok(section) } @@ -122,7 +124,10 @@ impl VkPayloadLayout { kind: PayloadSectionKind, commitments: usize, ) -> Result { - self.reserve(kind, commitments * G1_WORDS) + let word_len = commitments.checked_mul(G1_WORDS).ok_or_else(|| { + format!("VK payload G1 word overflow for {kind:?}: {commitments} commitments") + })?; + self.reserve(kind, word_len) } /// Return the section for `kind`, if it has been reserved. diff --git a/proofs/solidity-verifier/src/lowering/plan.rs b/proofs/solidity-verifier/src/lowering/plan.rs index e1682f287..6c09c98b1 100644 --- a/proofs/solidity-verifier/src/lowering/plan.rs +++ b/proofs/solidity-verifier/src/lowering/plan.rs @@ -12,7 +12,9 @@ use crate::lowering::{ kzg, layout, layout::memory::{PcsMemoryRequirements, VerifierMemoryLayout, VerifierMemoryLayoutConfig}, quotient::{QuotientComputationBlocks, QuotientHelperFlags, QuotientStateSlots}, - quotient_numerator::vm::{QuotientProgramBuild, QuotientProgramPlan, RepackedProofLayoutPlan}, + quotient_numerator::vm::{ + self as vm, certify, QuotientProgramBuild, QuotientProgramPlan, RepackedProofLayoutPlan, + }, render::{Halo2VerifyingKey, QuotientExternal, QuotientProgram}, VerifierBuildInputs, }; @@ -142,6 +144,8 @@ impl LoweringPlan { vk_mptr, &memory, "ient_plan.selector_fold, + quotient_operand_bounds(&meta, &data, &vk, vk_mptr, &memory), + meta.num_simple_selectors, ); let plan = Self { @@ -163,6 +167,23 @@ impl LoweringPlan { }; plan.validate_generator_invariants() .unwrap_or_else(|err| panic!("generator invariant violation: {err}")); + + // Certify the limb superinstructions against a generic-opcode build of + // the same identity stream. This needs `inputs`, so it runs here rather + // than inside `validate_generator_invariants`. + let baseline_build = inputs.build_quotient_program_items_with_limb_ops( + &plan.quotient.plan.items, + &plan.quotient.plan.selector_fold, + false, + ); + certify::certify_quotient_builds_agree( + &plan.quotient.plan, + &plan.quotient.build, + &baseline_build, + &plan.vk, + ) + .unwrap_or_else(|err| panic!("quotient dual-build certification failed: {err}")); + plan } @@ -279,6 +300,35 @@ impl LoweringPlan { self.vk.quotient_program_words )); } + // The VK payload embeds the const table and packed bytecode compiled + // inside `generate_vk`, but the interpreter is rendered from this + // independently recompiled `self.quotient.build`. Length checks alone + // let a nondeterministic/order-dependent compile ship a pinned VK whose + // bytecode disagrees with the rendered VM. Compare the embedded words + // against the plan build word-for-word so any divergence fails codegen. + if let Some(const_offset) = self.vk.quotient_const_offset_words { + let build_consts = &self.quotient.build.consts; + let embedded = &self.vk.constants[const_offset..const_offset + build_consts.len()]; + if embedded.iter().map(|(_, value)| value).ne(build_consts.iter()) { + return Err( + "quotient const table embedded in the VK payload does not match the \ + plan-rebuilt const table" + .to_string(), + ); + } + } + if let Some(program_offset) = self.vk.quotient_program_offset_words { + let build_words = + layout::vk_payload::PackedProgramCodec::encode_words(&self.quotient.build.bytes); + let embedded = &self.vk.constants[program_offset..program_offset + build_words.len()]; + if embedded.iter().map(|(_, value)| value).ne(build_words.iter()) { + return Err( + "quotient program bytecode embedded in the VK payload does not match the \ + plan-rebuilt bytecode" + .to_string(), + ); + } + } if self.quotient.program.stack_mptr != self.quotient.stack_mptr { return Err(format!( "quotient stack pointer drifted: model={:#x} planned={:#x}", @@ -291,9 +341,59 @@ impl LoweringPlan { self.quotient.program.eval_numer_mptr, self.quotient.state_slots.eval_numer_mptr )); } + // Bounds-check every address the emitted bytecode loads from against + // the windows this layout actually populates before the VM runs. + // Certification cannot do this: it compares the bytecode against the + // expression tree, so a pointer that is wrong in both agrees with + // itself. + vm::validate_quotient_mem_ptrs(&self.quotient.build.bytes, &self.quotient_read_model()) + .map_err(|err| format!("quotient memory pointer validation failed: {err}"))?; + // Prove the emitted bytecode still evaluates the identities it was + // lowered from, before it can be pinned into a verifying key. + certify::certify_quotient_program(&self.quotient.plan, &self.quotient.build, &self.vk) + .map_err(|err| format!("quotient program certification failed: {err}"))?; Ok(()) } + /// Addresses the compact quotient VM is allowed to load from. + /// + /// These are exactly the ranges the verifier has populated by the time the + /// VM runs, and they are the same ranges + /// `Halo2QuotientEvaluator::validate_layout` requires the external frame to + /// contain -- a read outside them is either uninitialized memory in the + /// split path or live verifier state in the inline path. + pub(crate) fn quotient_read_model(&self) -> vm::QuotientReadModel { + vm::QuotientReadModel { + windows: quotient_read_windows( + &self.meta, + &self.data, + &self.vk, + self.vk_mptr, + &self.memory, + ), + token_bases: vec![ + (vm::Q_MEM_L0, self.memory.l_0_mptr.value().as_usize()), + (vm::Q_MEM_L_LAST, self.memory.l_last_mptr.value().as_usize()), + ( + vm::Q_MEM_L_BLIND, + self.memory.l_blind_mptr.value().as_usize(), + ), + (vm::Q_MEM_BETA, self.memory.beta_mptr.value().as_usize()), + (vm::Q_MEM_GAMMA, self.memory.gamma_mptr.value().as_usize()), + (vm::Q_MEM_X, self.memory.x_mptr.value().as_usize()), + (vm::Q_MEM_THETA, self.memory.theta_mptr.value().as_usize()), + ( + vm::Q_MEM_TRASH_CHALLENGE, + self.memory.trash_challenge_mptr.value().as_usize(), + ), + ( + vm::Q_MEM_INSTANCE_EVAL, + self.memory.instance_eval_mptr.value().as_usize(), + ), + ], + } + } + /// Number of `(G1, scalar)` terms required by the optional accumulator MSM. fn acc_msm_terms(inputs: &VerifierBuildInputs<'_, '_>) -> usize { inputs @@ -307,3 +407,74 @@ impl LoweringPlan { .unwrap_or(0) } } + +/// The memory windows the compact quotient VM is allowed to load from, +/// shared by the build-time pointer validator +/// ([`LoweringPlan::quotient_read_model`]) and the runtime operand clamps +/// rendered into the interpreter (P12/L-6, +/// docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). One source of truth so the +/// two layers cannot drift. +pub(crate) fn quotient_read_windows( + meta: &ConstraintSystemMeta, + data: &Data, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + memory: &VerifierMemoryLayout, +) -> Vec { + let theta = data.theta_mptr.value().as_usize(); + let instance_eval = memory.instance_eval_mptr.value().as_usize(); + vec![ + vm::QuotientReadWindow { + name: "vk_payload", + start: vk_mptr.value().as_usize(), + len: vk.len(), + }, + vm::QuotientReadWindow { + name: "user_challenges", + start: data.challenge_mptr.value().as_usize(), + len: meta.num_user_challenges.iter().sum::() * layout::memory::WORD_BYTES, + }, + vm::QuotientReadWindow { + name: "challenge_and_common_slots", + // Ends one word past `instance_eval`, matching the frame + // window in `Halo2QuotientEvaluator::validate_layout`. + // `quotient_eval` sits immediately above and is a write + // target, not a VM input. + start: theta, + len: (instance_eval + layout::memory::WORD_BYTES).saturating_sub(theta), + }, + vm::QuotientReadWindow { + name: "decoded_proof_evals", + start: memory.reversed_evals_mptr.value().as_usize(), + len: meta.num_evals * layout::memory::WORD_BYTES, + }, + ] +} + +/// The coarse `[lo, hi]` bound over every non-empty read window, rendered +/// into the interpreter's operand clamps. `hi` is the last legally loadable +/// word address (inclusive). Build-time validation still enforces exact +/// per-window membership; the runtime clamp is defence in depth for a VM +/// whose program bytes are trusted only through the VK codehash pin. +pub(crate) fn quotient_operand_bounds( + meta: &ConstraintSystemMeta, + data: &Data, + vk: &Halo2VerifyingKey, + vk_mptr: Ptr, + memory: &VerifierMemoryLayout, +) -> (usize, usize) { + let windows = quotient_read_windows(meta, data, vk, vk_mptr, memory); + let lo = windows + .iter() + .filter(|w| w.len > 0) + .map(|w| w.start) + .min() + .expect("at least one non-empty quotient read window"); + let hi = windows + .iter() + .filter(|w| w.len > 0) + .map(|w| w.start + w.len - layout::memory::WORD_BYTES) + .max() + .expect("at least one non-empty quotient read window"); + (lo, hi) +} diff --git a/proofs/solidity-verifier/src/lowering/protocol/mod.rs b/proofs/solidity-verifier/src/lowering/protocol/mod.rs index a035abaf5..02ed0efdb 100644 --- a/proofs/solidity-verifier/src/lowering/protocol/mod.rs +++ b/proofs/solidity-verifier/src/lowering/protocol/mod.rs @@ -324,10 +324,31 @@ impl ProtocolPlan { /// passed to `partially_evaluate_identities` and KZG `multi_prepare`. /// The plan preserves that order so the Solidity transcript and proof /// cursors stay byte-compatible with the Rust verifier. + /// + /// Panics if the resulting plan fails [`ProtocolPlan::validate`]. Callers + /// on a fallible path — notably [`SolidityGenerator::try_new`], which + /// promises a typed error for unsupported constraint systems — must use + /// [`ProtocolPlan::try_from_constraint_system`] instead. Every production + /// path is fallible, so this panicking form is test-only. + #[cfg(test)] pub(crate) fn from_constraint_system( cs: &ConstraintSystem, nb_committed_instances: usize, ) -> Self { + Self::try_from_constraint_system(cs, nb_committed_instances) + .unwrap_or_else(|err| panic!("invalid protocol plan: {err}")) + } + + /// Fallible counterpart of the test-only + /// `ProtocolPlan::from_constraint_system`. + /// + /// Returns the validation failure rather than panicking, so constraint + /// systems outside the supported verifier shape can be surfaced as a typed + /// error at the public API boundary. + pub(crate) fn try_from_constraint_system( + cs: &ConstraintSystem, + nb_committed_instances: usize, + ) -> Result { let cs_degree = cs.degree(); let num_fixeds = cs.num_fixed_columns(); let permutation_columns = cs.permutation().get_columns(); @@ -444,6 +465,35 @@ impl ProtocolPlan { // Read (num_fixed_columns - num_simple_selectors) fixed evaluations. // Simple selector columns are intentionally absent from the proof // scalar stream and are filled by the quotient/linearization path. + // + // I-6 (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): the + // midnight-proofs verifier sizes this proof section COLUMN-based + // (`num_fixed_columns - num_simple_selectors`) while this generator + // sizes it QUERY-based (non-simple fixed queries). The two agree + // only when every non-simple fixed column is queried exactly once: + // a rotated or repeated fixed query would make the native verifier + // under-read and the generated verifier over-read the same proof + // bytes, silently desynchronizing every later transcript offset. + // Reject the divergence at plan time instead of inheriting it. + let non_simple_fixed_queries = fixed_queries + .iter() + .filter(|q| !simple_selector_cols.contains(&q.column)) + .count(); + assert_eq!( + non_simple_fixed_queries, + num_fixeds - simple_selector_cols.len(), + "fixed-eval section mismatch: {non_simple_fixed_queries} non-simple fixed \ + quer{} vs {} fixed columns minus {} simple selectors; the native verifier \ + reads the column-based count while this generator reads the query-based \ + count, so the proof scalar stream would desynchronize", + if non_simple_fixed_queries == 1 { + "y" + } else { + "ies" + }, + num_fixeds, + simple_selector_cols.len(), + ); proof.evals.extend( fixed_queries .iter() @@ -610,8 +660,8 @@ impl ProtocolPlan { common_polys, quotient, }; - plan.validate().unwrap_or_else(|err| panic!("invalid protocol plan: {err}")); - plan + plan.validate()?; + Ok(plan) } /// Number of scalar evaluations in the proof's main eval block. diff --git a/proofs/solidity-verifier/src/lowering/quotient.rs b/proofs/solidity-verifier/src/lowering/quotient.rs index d77a5e504..9a09a1f8c 100644 --- a/proofs/solidity-verifier/src/lowering/quotient.rs +++ b/proofs/solidity-verifier/src/lowering/quotient.rs @@ -142,6 +142,7 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { /// VK payload reservation, and the generated Yul interpreter. Keeping the /// bounds checks here prevents the standalone evaluator and in-verifier VM /// paths from drifting. + #[allow(clippy::too_many_arguments)] pub(super) fn quotient_template_program( &self, build: QuotientProgramBuild, @@ -149,6 +150,8 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { vk_mptr: Ptr, memory: &VerifierMemoryLayout, selector_fold: &SelectorFoldPlan, + operand_bounds: (usize, usize), + num_selector_buckets: usize, ) -> (QuotientProgram, usize, QuotientStateSlots) { let quotient_program_chunks = PackedProgramCodec::encode_words(&build.bytes); let quotient_const_words = vk.quotient_const_words; @@ -184,7 +187,12 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { selector_max_power: selector_fold.max_power, selector_tail_updates: Self::selector_tail_updates(selector_fold), stack_mptr: quotient_stack_mptr, + stack_hi: memory.quotient_stack_hi, + num_consts: build.consts.len(), program_mptr, + operand_lo: operand_bounds.0, + operand_hi: operand_bounds.1, + num_selector_buckets, }; (program, quotient_stack_mptr, state_slots) @@ -253,7 +261,17 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { // `build.max_stack` only describes the interpreted operand stack. Some // native callbacks share `quotient_stack_mptr` as a scratch base, so // the registered memory region must cover both possible users. - build.max_stack.max(native_callback_scratch_words) + // + // Floor at one word: every inline/native direct_quotient_block writes + // one eval-scratch word at eval_scratch_slot == quotient_stack_mptr + // (see direct_quotient_block and compact_quotient_computation_blocks), + // so the region is always written even when the interpreted stack and + // native scratch are both empty (a degenerate-but-valid VK whose gates + // all fit the inline prefix with no permutation sets, lookups, or VM + // items). Accounting that word here keeps the in-bounds invariant with + // the code that emits the write, rather than relying on the unrelated + // MODEXP-frame clamp in layout/memory.rs. + build.max_stack.max(native_callback_scratch_words).max(1) } /// Number of persistent VM temp words needed for state plus selector @@ -594,12 +612,28 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { } /// Estimate the generated native callback block for one identity. + /// + /// This is a size/gas PROXY consumed only by the native-vs-VM gate + /// selection heuristic (native_gate_candidates); it is never emitted. The + /// real native blocks are produced by compact_quotient_computation_blocks + /// with `selector_fold.gap_for(identity)`, but that fold plan does not + /// exist yet at selection time (it is derived from the selection + /// outcome), so we use a fixed proxy gap: Some(1) for selector targets, + /// None otherwise. + /// + /// Any divergence between this proxy and the eventual `gap_for` only + /// perturbs which gates get promoted to native callbacks; it can never + /// change the correctness of the generated verifier, because actual + /// emission always uses the real gap. Do not "fix" this to call + /// `gap_for` here: the plan is intentionally unavailable at this point. fn native_identity_estimate_block(identity: &QuotientIdentity) -> Vec { let state_slots = QuotientStateSlots { eval_numer_mptr: 0x2000, trace_id_mptr: 0x2020, selector_power_mptr: 0x2040, }; + // Proxy gap only; see the doc comment above. This deliberately differs + // from selector_fold.gap_for(identity) used at emission time. let selector_gap = matches!(identity.target, QuotientTarget::Selector(_)).then_some(1); Self::direct_quotient_block( &identity.lines, @@ -1262,15 +1296,26 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { yul_const_value(value, const_vars).as_deref() == Some(expected_coeff) } - /// Record `let name := const` bindings for later limb-chain matching. + /// Track constant variable bindings for later limb-chain matching. + /// + /// Records `name := const` (whether or not introduced with `let`), and, + /// crucially, forgets any variable that is reassigned to a non-constant + /// value. Ignoring non-`let` reassignments would leave a stale literal in + /// `const_vars`, so a later `mulmod(name, limb, r)` could be mis-recognized + /// as a fused limb7 coefficient and bake the wrong constant into the + /// generated quotient identity. fn record_yul_const_assignment(line: &str, const_vars: &mut HashMap) { - let Some((dst, rhs)) = yul_let_assignment(line) else { - return; - }; - let Some(value) = yul_const_value(&rhs, const_vars) else { + let Some(assignment) = yul_assignment(line) else { return; }; - const_vars.insert(dst, value); + match yul_const_value(&assignment.expr, const_vars) { + Some(value) => { + const_vars.insert(assignment.dst, value); + } + None => { + const_vars.remove(&assignment.dst); + } + } } /// Trace, advance, and accumulate one main quotient identity value. @@ -1589,6 +1634,18 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { Self::push_structured_main_fold(&mut block, "q_lookup_eval", state_slots, trace); block.push("}".to_string()); + // Fail closed on a chunk/helper-eval count mismatch: zip would + // otherwise silently drop the excess chunks, removing helper + // constraints from the y-batched numerator. Guarded indirectly today + // by the protocol lookup count check, but assert it directly at the + // zip site. + assert_eq!( + chunked.input_expression_chunks().len(), + h_evals.len(), + "lookup {lookup_idx}: input chunk count {} != helper eval count {}", + chunked.input_expression_chunks().len(), + h_evals.len(), + ); for (input_chunk, h_eval) in chunked.input_expression_chunks().iter().zip(h_evals.iter()) { @@ -1596,7 +1653,15 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { block.push("{".to_string()); if k == 0 { - block.push("let q_lookup_eval := 0".to_string()); + // Unreachable today (chunks are never empty), but emit the + // reference-faithful value (h_eval) rather than 0: the + // native verifier (plonk/logup.rs) computes + // helper_eval * (empty product = 1) - (empty sum = 0) + // = helper_eval for an empty chunk, enforcing h == 0. + // Emitting 0 leaves h unconstrained while the accumulator + // still folds this h_eval into sum_h. Mirrors the fix in + // quotient_numerator/yul_emit.rs. + block.push(format!("let q_lookup_eval := {}", h_eval)); Self::push_structured_main_fold( &mut block, "q_lookup_eval", @@ -1903,7 +1968,26 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { items: &[QuotientProgramItem], selector_fold: &SelectorFoldPlan, ) -> QuotientProgramBuild { - let mut builder = QuotientProgramBuilder::default(); + self.build_quotient_program_items_with_limb_ops( + items, + selector_fold, + crate::lowering::config::DEFAULT_QUOTIENT_LIMB_VM_OPS, + ) + } + + /// Lower the same item stream under an explicit limb-opcode policy. + /// + /// Rendering always uses the crate default. The `false` build exists so the + /// generator can cross-check the limb superinstructions against a program + /// that uses only generic Fr opcodes; see + /// `quotient_numerator::vm::certify::certify_quotient_builds_agree`. + pub(super) fn build_quotient_program_items_with_limb_ops( + &self, + items: &[QuotientProgramItem], + selector_fold: &SelectorFoldPlan, + limb_vm_ops: bool, + ) -> QuotientProgramBuild { + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(limb_vm_ops); // Lower the logical plan into bytecode in one pass. Repeated // subexpressions are emitted directly; native callbacks remain opaque // markers because their arithmetic is emitted as separate Yul kernels diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/certify.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/certify.rs new file mode 100644 index 000000000..22dcc0456 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/certify.rs @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Render-time self-certification of the emitted quotient VM program. +//! +//! The quotient lowering runs a peephole optimizer: shape recognizers rewrite +//! seven-limb foreign-field expressions into superinstructions, and a +//! run-compaction pass rewrites adjacent affine terms into counted opcodes. +//! Both are pure encoding choices that must preserve the evaluated polynomial +//! exactly. This module proves that they did, for the specific program this +//! render is about to emit, by executing the finalized bytecode with the +//! independent interpreter in [`super::reference`] and comparing each identity +//! against direct evaluation of the [`QuotientExpr`] tree it was lowered from. +//! +//! This is a generator-time gate, not a test: every artifact the generator +//! produces is certified before it can be pinned into a verifying key, so a +//! recognizer bug on a previously unseen gate shape fails the render instead of +//! shipping a wrong verifier. +//! +//! See [`super::reference`] for what this does and does not cover. + +use ff::Field; +use midnight_curves::Fq; +use sha3::{Digest, Keccak256}; + +use super::{ + quotient_op_len, + reference::{eval_quotient_expr, eval_quotient_identity, QuotientRefMemory}, + QuotientExpr, QuotientMem, QuotientProgramBuild, QuotientProgramItem, QuotientProgramPlan, + QuotientTarget, Q_OP_FOLD_MAIN, Q_OP_FOLD_SELECTOR, Q_OP_NATIVE_IDENTITY, Q_OP_NATIVE_LOOKUP, + Q_OP_NATIVE_PERMUTATION, +}; +use crate::lowering::render::Halo2VerifyingKey; + +/// Seed for the certification assignment. +/// +/// The seed is deterministic so failing renders reproduce exactly, but it is +/// derived from the finalized artifact rather than fixed globally. This keeps +/// the sampled assignment independent of any circuit author's pre-render view +/// of the quotient program while preserving reproducible diagnostics. +const QUOTIENT_CERTIFY_SEED_DOMAIN: &[u8] = b"midfall/quotient-vm/certify-seed/v3"; + +/// Derive the random-assignment seed from every compared artifact. +/// +/// Binding the oracle expressions closes the case where a miscompile drops a +/// tunable constant before it reaches the emitted constant table. Binding both +/// builds makes the dual-build challenge depend on the optimized and baseline +/// representations. The VK payload is included after generator invariants have +/// checked that its quotient sections match the finalized build. +pub(crate) fn derive_certify_seed( + builds: &[&QuotientProgramBuild], + exprs: &[&QuotientExpr], + vk_payload: &[u8], +) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(QUOTIENT_CERTIFY_SEED_DOMAIN); + + hasher.update((builds.len() as u64).to_be_bytes()); + for build in builds { + hasher.update((build.bytes.len() as u64).to_be_bytes()); + hasher.update(&build.bytes); + + hasher.update((build.consts.len() as u64).to_be_bytes()); + for value in &build.consts { + hasher.update(value.to_be_bytes::<32>()); + } + } + + hasher.update((exprs.len() as u64).to_be_bytes()); + for expr in exprs { + hash_quotient_expr(&mut hasher, expr); + } + + hasher.update((vk_payload.len() as u64).to_be_bytes()); + hasher.update(vk_payload); + + hasher.finalize().into() +} + +/// Hash one expression with explicit node and memory-address tags. +fn hash_quotient_expr(hasher: &mut Keccak256, expr: &QuotientExpr) { + match expr { + QuotientExpr::Const(value) => { + hasher.update([0]); + hasher.update(value.to_be_bytes::<32>()); + } + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => { + hasher.update([1]); + hasher.update(ptr.to_be_bytes()); + } + QuotientExpr::Mem(QuotientMem::Token(token)) => { + hasher.update([2, *token]); + } + QuotientExpr::Mem(QuotientMem::TokenOffset(token, offset)) => { + hasher.update([3, *token]); + hasher.update(offset.to_be_bytes()); + } + QuotientExpr::Add(lhs, rhs) => { + hasher.update([4]); + hash_quotient_expr(hasher, lhs); + hash_quotient_expr(hasher, rhs); + } + QuotientExpr::Mul(lhs, rhs) => { + hasher.update([5]); + hash_quotient_expr(hasher, lhs); + hash_quotient_expr(hasher, rhs); + } + QuotientExpr::Neg(inner) => { + hasher.update([6]); + hash_quotient_expr(hasher, inner); + } + } +} + +/// Expressions evaluated by the compact quotient program. +fn interpreted_exprs(plan: &QuotientProgramPlan) -> Vec<&QuotientExpr> { + plan.items + .iter() + .filter_map(|item| match item { + QuotientProgramItem::Identity(identity) => Some(&identity.expr), + _ => None, + }) + .collect() +} + +/// Certify that the emitted bytecode evaluates the planned identities. +/// +/// Runs after [`super::validate_quotient_program`], which has already proven +/// the stream decodes and is stack-safe; this pass assumes well-formedness and +/// checks *meaning*. +pub(crate) fn certify_quotient_program( + plan: &QuotientProgramPlan, + build: &QuotientProgramBuild, + vk: &Halo2VerifyingKey, +) -> Result<(), String> { + let exprs = interpreted_exprs(plan); + let vk_payload = vk.bytes(); + let seed = derive_certify_seed(&[build], &exprs, &vk_payload); + let mut mem = QuotientRefMemory::new(seed); + let bytes = &build.bytes; + let mut cursor = 0usize; + + for (item_idx, item) in plan.items.iter().enumerate() { + match item { + QuotientProgramItem::Identity(identity) => { + let (expr_end, fold_op) = identity_segment(bytes, cursor) + .map_err(|err| format!("quotient item {item_idx}: {err}"))?; + + let actual = + eval_quotient_identity(&bytes[cursor..expr_end], &build.consts, &mut mem) + .map_err(|err| { + format!( + "quotient identity {} ({:?}): {err}", + identity.meta.global_index, identity.meta.source + ) + })?; + let expected = eval_quotient_expr(&identity.expr, &mut mem); + + if actual != expected { + return Err(format!( + "quotient VM miscompiled identity {} ({:?}): bytecode evaluates to {:?} \ + but its expression evaluates to {:?}. This is a codegen bug in the \ + quotient lowering (shape recognizer, operand packing, or run \ + compaction), not a proof or verifying-key problem.", + identity.meta.global_index, identity.meta.source, actual, expected + )); + } + + check_fold(bytes, expr_end, fold_op, identity, plan).map_err(|err| { + format!( + "quotient identity {} ({:?}): {err}", + identity.meta.global_index, identity.meta.source + ) + })?; + + cursor = expr_end + quotient_op_len(bytes, expr_end); + } + QuotientProgramItem::NativePermutation + | QuotientProgramItem::NativeLookup + | QuotientProgramItem::NativeIdentity(_) => { + // Native markers carry no arithmetic here: the Yul template + // substitutes generated straight-line kernels. Certify only + // that the marker sits at the planned stream position. + let expected_op = match item { + QuotientProgramItem::NativePermutation => Q_OP_NATIVE_PERMUTATION, + QuotientProgramItem::NativeLookup => Q_OP_NATIVE_LOOKUP, + _ => Q_OP_NATIVE_IDENTITY, + }; + let actual_op = *bytes.get(cursor).ok_or_else(|| { + format!("quotient item {item_idx}: program ended before native marker") + })?; + if actual_op != expected_op { + return Err(format!( + "quotient item {item_idx}: expected native marker {expected_op:#x} at byte \ + {cursor}, found {actual_op:#x}" + )); + } + if let QuotientProgramItem::NativeIdentity(native_idx) = item { + // The marker index is a big-endian u16, not a single byte. + let hi = *bytes.get(cursor + 1).ok_or_else(|| { + format!("quotient item {item_idx}: truncated native identity index") + })?; + let lo = *bytes.get(cursor + 2).ok_or_else(|| { + format!("quotient item {item_idx}: truncated native identity index") + })?; + let encoded = u16::from_be_bytes([hi, lo]) as usize; + if encoded != *native_idx { + return Err(format!( + "quotient item {item_idx}: native identity index {encoded} does not \ + match planned index {native_idx}" + )); + } + } + cursor += quotient_op_len(bytes, cursor); + } + } + } + + if cursor != bytes.len() { + return Err(format!( + "quotient program has {} trailing byte(s) after the planned item stream", + bytes.len() - cursor + )); + } + + Ok(()) +} + +/// Find the end of one identity expression and the fold opcode that closes it. +fn identity_segment(bytes: &[u8], start: usize) -> Result<(usize, u8), String> { + let mut idx = start; + while idx < bytes.len() { + let op = bytes[idx]; + match op { + Q_OP_FOLD_MAIN | Q_OP_FOLD_SELECTOR => return Ok((idx, op)), + Q_OP_NATIVE_PERMUTATION | Q_OP_NATIVE_LOOKUP | Q_OP_NATIVE_IDENTITY => { + return Err(format!( + "native marker {op:#x} at byte {idx} interrupts an identity expression" + )); + } + _ => idx += quotient_op_len(bytes, idx), + } + } + Err(format!( + "identity expression starting at byte {start} is never folded" + )) +} + +/// Check that the emitted fold matches the planned target and selector gap. +fn check_fold( + bytes: &[u8], + fold_idx: usize, + fold_op: u8, + identity: &super::QuotientIdentity, + plan: &QuotientProgramPlan, +) -> Result<(), String> { + match (fold_op, identity.target) { + (Q_OP_FOLD_MAIN, QuotientTarget::Main) => Ok(()), + (Q_OP_FOLD_SELECTOR, QuotientTarget::Selector(selector_idx)) => { + let encoded_idx = *bytes + .get(fold_idx + 1) + .ok_or_else(|| "truncated selector fold index".to_string())? + as usize; + if encoded_idx != selector_idx { + return Err(format!( + "selector fold targets bucket {encoded_idx} but the plan says {selector_idx}" + )); + } + let hi = *bytes + .get(fold_idx + 2) + .ok_or_else(|| "truncated selector fold gap".to_string())?; + let lo = *bytes + .get(fold_idx + 3) + .ok_or_else(|| "truncated selector fold gap".to_string())?; + let encoded_gap = u16::from_be_bytes([hi, lo]) as usize; + let planned_gap = plan + .selector_fold + .gap_for(identity) + .ok_or_else(|| "selector identity has no planned fold gap".to_string())?; + if encoded_gap != planned_gap { + return Err(format!( + "selector fold gap {encoded_gap} does not match planned gap {planned_gap}" + )); + } + Ok(()) + } + (fold_op, target) => Err(format!( + "fold opcode {fold_op:#x} does not match planned target {target:?}" + )), + } +} + +/// Certify that two builds of the same identity stream agree. +/// +/// The limb-aware superinstructions are the least principled part of the +/// lowering: they pattern-match algebraic shapes out of a commutative-ring +/// expression tree. Building the same stream with those recognizers disabled +/// yields a program using only `PUSH`/`ADD`/`MUL`/`NEG`, which is +/// straightforward to audit. Requiring the two to agree identity-by-identity +/// turns every recognizer from trusted code into a checked optimization. +pub(crate) fn certify_quotient_builds_agree( + plan: &QuotientProgramPlan, + optimized: &QuotientProgramBuild, + baseline: &QuotientProgramBuild, + vk: &Halo2VerifyingKey, +) -> Result<(), String> { + let exprs = interpreted_exprs(plan); + let vk_payload = vk.bytes(); + let seed = derive_certify_seed(&[optimized, baseline], &exprs, &vk_payload); + let mut mem = QuotientRefMemory::new(seed); + + let optimized_values = identity_values(plan, optimized, &mut mem)?; + let baseline_values = identity_values(plan, baseline, &mut mem)?; + + if optimized_values.len() != baseline_values.len() { + return Err(format!( + "quotient dual build disagrees on identity count: {} with limb opcodes, {} without", + optimized_values.len(), + baseline_values.len() + )); + } + + for (position, (lhs, rhs)) in optimized_values.iter().zip(baseline_values.iter()).enumerate() { + if lhs != rhs { + return Err(format!( + "quotient limb superinstructions changed the value of interpreted identity at \ + stream position {position}: {lhs:?} with limb opcodes, {rhs:?} without. One of \ + the shape recognizers is unsound for this gate shape." + )); + } + } + + Ok(()) +} + +/// Evaluate every interpreted identity in one build, in stream order. +fn identity_values( + plan: &QuotientProgramPlan, + build: &QuotientProgramBuild, + mem: &mut QuotientRefMemory, +) -> Result, String> { + let bytes = &build.bytes; + let mut values = Vec::new(); + let mut cursor = 0usize; + + for item in &plan.items { + match item { + QuotientProgramItem::Identity(_) => { + let (expr_end, _) = identity_segment(bytes, cursor)?; + values.push(eval_quotient_identity( + &bytes[cursor..expr_end], + &build.consts, + mem, + )?); + cursor = expr_end + quotient_op_len(bytes, expr_end); + } + _ => { + // Native markers evaluate no bytecode; both builds emit the + // same marker at the same stream position. + values.push(Fq::ZERO); + cursor += quotient_op_len(bytes, cursor); + } + } + } + + Ok(values) +} diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs index affd899a2..c993c5351 100644 --- a/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/mod.rs @@ -52,6 +52,9 @@ //! underflow, and identity-boundary stack leaks before the bytes can be pinned //! into a VK runtime. +pub(crate) mod certify; +pub(crate) mod reference; + use std::collections::{HashMap, HashSet}; use ff::{Field, PrimeField}; @@ -1096,7 +1099,6 @@ impl Default for QuotientProgramBuilder { impl QuotientProgramBuilder { /// Create a builder, optionally enabling limb-specialized opcode emission. - #[cfg(test)] pub(crate) fn with_limb_vm_ops(enabled: bool) -> Self { Self { limb_vm_ops: enabled, @@ -1210,6 +1212,8 @@ impl QuotientProgramBuilder { let bytes = compact_quotient_runs(&self.bytes); let validated_max_stack = validate_quotient_program(&bytes) .unwrap_or_else(|err| panic!("invalid finalized quotient VM program: {err}")); + validate_quotient_const_slots(&bytes, self.consts.len()) + .unwrap_or_else(|err| panic!("invalid finalized quotient VM program: {err}")); assert_eq!( validated_max_stack, self.max_stack, "quotient VM physical program stack depth diverged from builder accounting" @@ -1357,18 +1361,49 @@ impl QuotientProgramBuilder { if !self.limb_vm_ops { return false; } - let Some((shape, residue)) = quotient_limb_subshape(expr) else { + let Some((shape, residue, matched)) = quotient_limb_subshape(expr) else { return false; }; if !self.limb_shape_has_u8_const_slots(&shape) { return false; } + self.reserve_limb_shape_consts(&shape); self.emit_expr(&residue); - self.emit_limb_shape(shape); + // `limb_shape_has_u8_const_slots` was checked against the constant table + // before `emit_expr(&residue)`. Emitting the residue can insert new + // constants and push a shape coefficient past a one-byte constant slot, + // which would panic in `emit_limb_shape`'s `u8::try_from(...).expect(...)`. + // Re-check against the post-residue table: keep the fused limb opcode + // only while every coefficient still fits, otherwise emit the matched + // terms through the generic path (their sum equals the shape's value). + if self.limb_shape_has_u8_const_slots(&shape) { + self.emit_limb_shape(shape); + } else { + self.emit_affine_terms(&matched); + } self.op_binary(Q_OP_ADD); true } + /// Emit `Σ terms[i]` with generic stack ops, leaving one value on the + /// stack. + /// + /// Each entry is one recognized affine/bilinear term (a scaled memory load + /// or product), so emitting it cannot re-enter the limb-decomposition + /// peephole, and its coefficients use `emit_const`'s u16-capable slots. + /// This is the panic-free fallback for a recognized limb shape whose + /// coefficients no longer fit one-byte constant slots after intervening + /// emission. The slice is always non-empty (a recognized subshape uses + /// at least one term). + fn emit_affine_terms(&mut self, terms: &[QuotientExpr]) { + for (idx, term) in terms.iter().enumerate() { + self.emit_expr(term); + if idx > 0 { + self.op_binary(Q_OP_ADD); + } + } + } + /// Try to replace a full expression with one limb-specialized opcode. /// /// The const-slot preflight is part of the ABI justification: limb opcodes @@ -1428,6 +1463,22 @@ impl QuotientProgramBuilder { self.peek_u8_const_slots(&coeffs).is_some() } + /// Reserve coefficient slots before residue emission can grow the table. + fn reserve_limb_shape_consts(&mut self, shape: &QuotientLimbShape) { + match shape { + QuotientLimbShape::Lin7 { terms } | QuotientLimbShape::Bilin7Row { terms, .. } => { + for (coeff, _) in terms { + self.const_slot(*coeff); + } + } + QuotientLimbShape::Bilin7Pairwise { coeffs, .. } => { + for coeff in coeffs { + self.const_slot(*coeff); + } + } + } + } + /// Emit the byte-level representation of a pre-validated limb shape. fn emit_limb_shape(&mut self, shape: QuotientLimbShape) { match shape { @@ -1589,15 +1640,39 @@ impl QuotientProgramBuilder { if !collect_product_leaves(product, &mut leaves) { return false; } - let Some(product) = self.product_add_macro(&leaves) else { + let Some(fused) = self.product_add_macro(&leaves) else { return false; }; + self.reserve_product_add_consts(fused); self.emit_expr(base); - self.emit_product_add(product); + // `product_add_macro` checked the fused scalar against the constant + // table as it stood *before* `emit_expr(base)`. Emitting `base` can + // insert new constants and push that scalar past a one-byte constant + // slot, which would panic in `emit_product_add`'s + // `u8::try_from(...).expect(...)`. Re-check against the post-`base` + // table: keep the fused opcode only while the scalar still fits, + // otherwise add the product through the generic path (its lone scalar + // goes through `emit_const`, which falls back to a u16 slot). + if self.product_add_fits_u8_slot(&fused) { + self.emit_product_add(fused); + } else { + self.emit_expr(product); + self.op_binary(Q_OP_ADD); + } true } + /// Whether the fused product-add scalar (if any) still lands in a one-byte + /// constant slot given the current constant table. + fn product_add_fits_u8_slot(&self, product: &QuotientProductAdd) -> bool { + match *product { + QuotientProductAdd::MemMemConstU8 { scalar, .. } + | QuotientProductAdd::ConstU8Mem { scalar, .. } => self.const_fits_u8_slot(scalar), + QuotientProductAdd::MemMem { .. } => true, + } + } + /// Recognize product leaves that can be encoded as one fused add-mul op. /// /// The fused forms require literal `u16` memory pointers and, where a @@ -1641,6 +1716,17 @@ impl QuotientProgramBuilder { } } + /// Reserve coefficient slots before another expression can grow the table. + fn reserve_product_add_consts(&mut self, product: QuotientProductAdd) { + match product { + QuotientProductAdd::MemMemConstU8 { scalar, .. } + | QuotientProductAdd::ConstU8Mem { scalar, .. } => { + self.const_slot(scalar); + } + QuotientProductAdd::MemMem { .. } => {} + } + } + /// Emit one fused add-mul accumulator operation. fn emit_product_add(&mut self, product: QuotientProductAdd) { match product { @@ -1969,6 +2055,463 @@ pub(crate) fn validate_quotient_program(bytes: &[u8]) -> Result { Ok(max_stack) } +/// Bounds-check every constant-table slot referenced by a finalized program. +/// +/// `validate_quotient_program` proves structural and stack safety but never +/// checks that decoded const-table indices fall inside the emitted table. An +/// encoder/planner regression that emits an out-of-range slot (this class has +/// already produced one real bug) would otherwise make the deployed verifier +/// load an arbitrary trailing VK word as a gate coefficient, silently flipping +/// accept/reject. Run this after `validate_quotient_program`, whose byte-length +/// validation guarantees the layout walked here is already in bounds. +pub(crate) fn validate_quotient_const_slots(bytes: &[u8], const_len: usize) -> Result<(), String> { + let check = |slot: usize, idx: usize| -> Result<(), String> { + if slot >= const_len { + return Err(format!( + "quotient VM const slot {slot} at byte {idx} is outside the {const_len}-entry constant table" + )); + } + Ok(()) + }; + let limb_stride = 1 + QUOTIENT_VM_BYTE_U16_BYTES; + + for (idx, op, _len) in quotient_bytecode_ops(bytes) { + match op { + Q_OP_PUSH_CONST | Q_OP_ADD_CONST | Q_OP_MUL_CONST => { + check(read_u16(bytes, idx + 1) as usize, idx)?; + } + Q_OP_PUSH_CONST_U8 | Q_OP_ADD_CONST_U8 | Q_OP_MUL_CONST_U8 => { + check(bytes[idx + 1] as usize, idx)?; + } + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => { + check(bytes[idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES] as usize, idx)?; + } + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => { + check( + bytes[idx + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES] as usize, + idx, + )?; + } + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + let count = read_u16(bytes, idx + 1) as usize; + let base = idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES; + let stride = QUOTIENT_VM_BYTE_U16_BYTES + 1; + for k in 0..count { + check( + bytes[base + k * stride + QUOTIENT_VM_BYTE_U16_BYTES] as usize, + idx, + )?; + } + } + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 => { + let count = read_u16(bytes, idx + 1) as usize; + let base = idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES; + let stride = 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1; + for k in 0..count { + check( + bytes[base + k * stride + 2 * QUOTIENT_VM_BYTE_U16_BYTES] as usize, + idx, + )?; + } + } + Q_OP_LIN7 => { + for k in 0..QUOTIENT_VM_LIMBS { + check(bytes[idx + 1 + k * limb_stride] as usize, idx)?; + } + } + Q_OP_BILIN7_ROW => { + let base = idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES; + for k in 0..QUOTIENT_VM_LIMBS { + check(bytes[base + k * limb_stride] as usize, idx)?; + } + } + Q_OP_BILIN7_PAIRWISE => { + let base = idx + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES; + for k in 0..QUOTIENT_VM_PAIRWISE_COEFFS { + check(bytes[base + k] as usize, idx)?; + } + } + Q_OP_AFFINE_SUM => { + let lin_count = read_u16(bytes, idx + 1) as usize; + let product_count = read_u16(bytes, idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES) as usize; + let mut cursor = idx + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES; + for _ in 0..lin_count { + check(bytes[cursor + QUOTIENT_VM_BYTE_U16_BYTES] as usize, idx)?; + cursor += QUOTIENT_VM_BYTE_U16_BYTES + 1; + } + for _ in 0..product_count { + check(bytes[cursor + 2 * QUOTIENT_VM_BYTE_U16_BYTES] as usize, idx)?; + cursor += 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1; + } + } + Q_OP_MODARITH7 => { + let mut cursor = idx + 1; + let flags = bytes[cursor]; + cursor += 1; + if flags & Q_MODARITH7_FLAG_COND != 0 { + cursor += QUOTIENT_VM_BYTE_U16_BYTES; + } + if flags & Q_MODARITH7_FLAG_CONST != 0 { + check(bytes[cursor] as usize, idx)?; + cursor += 1; + } + let lin_count = bytes[cursor] as usize; + let row_count = bytes[cursor + 1] as usize; + let pairwise_count = bytes[cursor + 2] as usize; + let mem_count = bytes[cursor + 3] as usize; + let product_count = bytes[cursor + 4] as usize; + cursor += 5; + for _ in 0..lin_count { + for _ in 0..QUOTIENT_VM_LIMBS { + check(bytes[cursor] as usize, idx)?; + cursor += limb_stride; + } + } + for _ in 0..row_count { + cursor += QUOTIENT_VM_BYTE_U16_BYTES; + for _ in 0..QUOTIENT_VM_LIMBS { + check(bytes[cursor] as usize, idx)?; + cursor += limb_stride; + } + } + for _ in 0..pairwise_count { + cursor += 2 * QUOTIENT_VM_BYTE_U16_BYTES; + for _ in 0..QUOTIENT_VM_PAIRWISE_COEFFS { + check(bytes[cursor] as usize, idx)?; + cursor += 1; + } + } + for _ in 0..mem_count { + check(bytes[cursor] as usize, idx)?; + cursor += limb_stride; + } + for _ in 0..product_count { + check(bytes[cursor] as usize, idx)?; + cursor += 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES; + } + } + _ => {} + } + } + + Ok(()) +} + +/// One byte range the compact quotient VM is allowed to load from. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct QuotientReadWindow { + /// Stable name used in validation errors. + pub(crate) name: &'static str, + /// Start byte offset in verifier memory. + pub(crate) start: usize, + /// Length in bytes. Zero-length windows never accept a pointer. + pub(crate) len: usize, +} + +impl QuotientReadWindow { + /// Whether a full 32-byte `mload` at `ptr` stays inside this window. + fn contains_word(&self, ptr: usize) -> bool { + self.len != 0 + && ptr >= self.start + && ptr.saturating_add(WORD_BYTES) <= self.start.saturating_add(self.len) + } +} + +/// Legal read set for one finalized quotient program. +/// +/// Built from the converged memory layout, so it describes the addresses this +/// concrete verifier actually populates before the VM runs. +#[derive(Clone, Debug, Default)] +pub(crate) struct QuotientReadModel { + /// Ranges the VM may load from. + pub(crate) windows: Vec, + /// Resolved base address for each symbolic memory token. + pub(crate) token_bases: Vec<(u8, usize)>, +} + +impl QuotientReadModel { + /// Resolve a symbolic memory token to its generated base address. + fn token_base(&self, token: u8) -> Option { + self.token_bases + .iter() + .find_map(|(candidate, base)| (*candidate == token).then_some(*base)) + } + + /// Name the window covering a word-sized load, if any. + fn window_for(&self, ptr: usize) -> Option<&'static str> { + self.windows + .iter() + .find(|window| window.contains_word(ptr)) + .map(|window| window.name) + } +} + +/// Bounds-check every memory pointer a finalized program loads from. +/// +/// `validate_quotient_const_slots` covers constant-table indices; this covers +/// the other half of the operand space. The pointers baked into the bytecode +/// are absolute generated addresses, so an emitter or planner regression that +/// computes one incorrectly makes the deployed verifier read a live challenge, +/// commitment word, or uninitialized scratch as a gate value -- silently +/// flipping accept/reject rather than reverting. +/// +/// Note what the render-time certification in [`certify`] does *not* +/// cover here. It compares the bytecode against the `QuotientExpr` tree it was +/// lowered from, and [`reference::QuotientRefMemory`] derives a value +/// from whatever address it is handed, so a pointer that disagrees between the +/// two shows up as a value mismatch. A pointer that is already wrong *in the +/// tree* -- a `Data` or planner bug upstream of the VM -- makes both sides +/// agree on the wrong address and certifies cleanly. This check is what catches +/// that class. +/// +/// Run after [`validate_quotient_program`], whose byte-length validation +/// guarantees the operand layout walked here is in bounds. +pub(crate) fn validate_quotient_mem_ptrs( + bytes: &[u8], + model: &QuotientReadModel, +) -> Result<(), String> { + for (idx, op, _len) in quotient_bytecode_ops(bytes) { + let mut check = |kind: &str, ptr: usize| -> Result<(), String> { + if !ptr.is_multiple_of(WORD_BYTES) { + return Err(format!( + "quotient VM {kind} pointer {ptr:#x} at byte {idx} is not 32-byte aligned" + )); + } + if model.window_for(ptr).is_none() { + return Err(format!( + "quotient VM {kind} pointer {ptr:#x} at byte {idx} is outside every window \ + the verifier populates before the quotient VM runs ({})", + describe_read_windows(model) + )); + } + Ok(()) + }; + quotient_read_pointers(bytes, idx, op, model, &mut check)?; + } + + Ok(()) +} + +/// Render the legal read set for a validation error message. +fn describe_read_windows(model: &QuotientReadModel) -> String { + model + .windows + .iter() + .map(|window| { + format!( + "{}=[{:#x}..{:#x})", + window.name, + window.start, + window.start + window.len + ) + }) + .collect::>() + .join(", ") +} + +/// Visit every memory address one instruction loads from. +/// +/// Unlike [`validate_quotient_const_slots`], the fallback arm is an error +/// rather than a no-op: a new opcode with pointer operands that forgets to +/// extend this walker fails the render instead of silently losing its bounds +/// check. `quotient_pointer_walker_covers_every_opcode` pins that the walker is +/// total over `QUOTIENT_OPCODE_TABLE`. +pub(crate) fn quotient_read_pointers( + bytes: &[u8], + idx: usize, + op: u8, + model: &QuotientReadModel, + check: &mut impl FnMut(&str, usize) -> Result<(), String>, +) -> Result<(), String> { + let u16_at = |offset: usize| read_u16(bytes, offset) as usize; + let u32_at = |offset: usize| { + u32::from_be_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) as usize + }; + // `const_slot, u16 ptr` -- the LIN7 limb operand and the MODARITH7 mem term. + let limb_terms = |base: usize, check: &mut dyn FnMut(&str, usize) -> Result<(), String>| { + for k in 0..QUOTIENT_VM_LIMBS { + check( + "limb", + u16_at(base + k * (1 + QUOTIENT_VM_BYTE_U16_BYTES) + 1), + )?; + } + Ok::<(), String>(()) + }; + // Both pairwise bases are indexed `base + i * 0x20` for seven limbs, so the + // whole span has to be in range, not just the base word. + let limb_span = + |base_ptr: usize, kind: &str, check: &mut dyn FnMut(&str, usize) -> Result<(), String>| { + for k in 0..QUOTIENT_VM_LIMBS { + check(kind, base_ptr + k * WORD_BYTES)?; + } + Ok::<(), String>(()) + }; + + match op { + // No memory operands. + Q_OP_PUSH_CONST + | Q_OP_PUSH_CONST_U8 + | Q_OP_ADD + | Q_OP_MUL + | Q_OP_NEG + | Q_OP_POW5 + | Q_OP_ADD_CONST + | Q_OP_ADD_CONST_U8 + | Q_OP_MUL_CONST + | Q_OP_MUL_CONST_U8 + | Q_OP_FOLD_MAIN + | Q_OP_FOLD_SELECTOR + | Q_OP_NATIVE_PERMUTATION + | Q_OP_NATIVE_LOOKUP + | Q_OP_NATIVE_IDENTITY => Ok(()), + + Q_OP_PUSH_MEM_LITERAL => check("literal", u32_at(idx + 1)), + Q_OP_PUSH_MEM_U16 | Q_OP_ADD_MEM_U16 | Q_OP_MUL_MEM_U16 => check("u16", u16_at(idx + 1)), + + Q_OP_PUSH_MEM_TOKEN | Q_OP_PUSH_MEM_TOKEN_OFFSET => { + let token = bytes[idx + 1]; + let base = model.token_base(token).ok_or_else(|| { + format!( + "quotient VM memory token {token:#x} at byte {idx} has no generated base \ + address in this layout" + ) + })?; + let offset = if op == Q_OP_PUSH_MEM_TOKEN_OFFSET { + u32_at(idx + 2) + } else { + 0 + }; + check("token", base + offset) + } + + Q_OP_ADD_MUL_MEM_MEM => { + check("u16", u16_at(idx + 1))?; + check("u16", u16_at(idx + 3)) + } + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => { + check("u16", u16_at(idx + 1))?; + check("u16", u16_at(idx + 3)) + } + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => check("u16", u16_at(idx + 1)), + + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 => { + let count = u16_at(idx + 1); + let base = idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES; + let stride = 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1; + for k in 0..count { + check("u16", u16_at(base + k * stride))?; + check( + "u16", + u16_at(base + k * stride + QUOTIENT_VM_BYTE_U16_BYTES), + )?; + } + Ok(()) + } + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + let count = u16_at(idx + 1); + let base = idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES; + let stride = QUOTIENT_VM_BYTE_U16_BYTES + 1; + for k in 0..count { + check("u16", u16_at(base + k * stride))?; + } + Ok(()) + } + Q_OP_AFFINE_SUM => { + let lin_count = u16_at(idx + 1); + let product_count = u16_at(idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES); + let mut cursor = idx + 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES; + for _ in 0..lin_count { + check("u16", u16_at(cursor))?; + cursor += QUOTIENT_VM_BYTE_U16_BYTES + 1; + } + for _ in 0..product_count { + check("u16", u16_at(cursor))?; + check("u16", u16_at(cursor + QUOTIENT_VM_BYTE_U16_BYTES))?; + cursor += 2 * QUOTIENT_VM_BYTE_U16_BYTES + 1; + } + Ok(()) + } + + Q_OP_LIN7 => limb_terms(idx + 1, check), + Q_OP_BILIN7_ROW => { + check("u16", u16_at(idx + 1))?; + limb_terms(idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES, check) + } + Q_OP_BILIN7_PAIRWISE => { + limb_span(u16_at(idx + 1), "pairwise_lhs", check)?; + limb_span( + u16_at(idx + 1 + QUOTIENT_VM_BYTE_U16_BYTES), + "pairwise_rhs", + check, + ) + } + Q_OP_MODARITH7 => { + let mut cursor = idx + 1; + let flags = bytes[cursor]; + cursor += 1; + let cond = if flags & Q_MODARITH7_FLAG_COND != 0 { + let ptr = u16_at(cursor); + cursor += QUOTIENT_VM_BYTE_U16_BYTES; + Some(ptr) + } else { + None + }; + if flags & Q_MODARITH7_FLAG_CONST != 0 { + cursor += 1; + } + let lin_count = bytes[cursor] as usize; + let row_count = bytes[cursor + 1] as usize; + let pairwise_count = bytes[cursor + 2] as usize; + let mem_count = bytes[cursor + 3] as usize; + let product_count = bytes[cursor + 4] as usize; + cursor += 5; + let limb_stride = QUOTIENT_VM_LIMBS * (1 + QUOTIENT_VM_BYTE_U16_BYTES); + for _ in 0..lin_count { + limb_terms(cursor, check)?; + cursor += limb_stride; + } + for _ in 0..row_count { + check("u16", u16_at(cursor))?; + cursor += QUOTIENT_VM_BYTE_U16_BYTES; + limb_terms(cursor, check)?; + cursor += limb_stride; + } + for _ in 0..pairwise_count { + limb_span(u16_at(cursor), "pairwise_lhs", check)?; + limb_span( + u16_at(cursor + QUOTIENT_VM_BYTE_U16_BYTES), + "pairwise_rhs", + check, + )?; + cursor += 2 * QUOTIENT_VM_BYTE_U16_BYTES + QUOTIENT_VM_PAIRWISE_COEFFS; + } + for _ in 0..mem_count { + check("u16", u16_at(cursor + 1))?; + cursor += 1 + QUOTIENT_VM_BYTE_U16_BYTES; + } + for _ in 0..product_count { + check("u16", u16_at(cursor + 1))?; + check("u16", u16_at(cursor + 1 + QUOTIENT_VM_BYTE_U16_BYTES))?; + cursor += 1 + 2 * QUOTIENT_VM_BYTE_U16_BYTES; + } + if let Some(cond) = cond { + check("modarith_cond", cond)?; + } + Ok(()) + } + + _ => Err(format!( + "quotient VM pointer walker does not handle opcode {op:#x} at byte {idx}; extend \ + `quotient_read_pointers` so the new opcode's memory operands stay bounds-checked" + )), + } +} + /// Decode one instruction and validate token operands. fn decode_byte_quotient_instruction(bytes: &[u8], idx: usize) -> Result<(u8, usize), String> { require_quotient_bytes(bytes, idx, 1, "opcode")?; @@ -2406,6 +2949,21 @@ impl QuotientExpressionEnv for DataQuotientExpressionEnv<'_> { .expect("committed instance eval present"), ) } else { + // The builder rejects rotated instance queries before lowering, so + // the direct public-input column always uses the one local + // Lagrange evaluation computed for `Rotation::cur()`. + // + // Hard assert (not debug_assert): `debug_assert` compiles out in + // release, and this is the last line of defense far from the + // constructor guard (builder/api.rs). Substituting the Rotation::cur + // eval for a rotated query would silently emit a verifier that + // evaluates the gate with instance(x) instead of instance(x*w^k), + // enforcing a different quotient identity than the circuit. Fail + // closed, matching word_to_quotient_expr / ptr_to_quotient_mem. + assert_eq!( + rotation, 0, + "rotated public instance query reached lowering" + ); word_to_quotient_expr(self.data.instance_eval) } } @@ -2435,17 +2993,40 @@ pub(crate) fn ptr_to_quotient_mem(ptr: Ptr) -> QuotientMem { ); match ptr.value() { Value::Integer(offset) => { - assert!(offset >= 0, "negative quotient memory pointer"); - QuotientMem::Literal(offset as u32) + // Checked conversion: Value offsets are isize, so `offset as u32` + // would silently wrap for a negative or > u32::MAX offset and make + // the VM mload an unrelated address (a verifier computing the + // quotient numerator from the wrong memory word, with no build-time + // diagnostic). Fail loudly instead, like the other narrowing casts + // in this file (u16::try_from, u8::try_from). + let offset = u32::try_from(offset) + .expect("quotient memory pointer must be a non-negative offset that fits in u32"); + // Every address the quotient VM reads is a 32-byte word slot (the + // layout allocates in WORD_BYTES units and all eval/challenge/VK/ + // scratch handles are word multiples). A non-word-aligned literal + // pointer signals a truncated/mis-encoded address that would make + // the VM mload a straddling window; reject it at the single + // construction choke point rather than emit a corrupt verifier. + assert!( + (offset as usize).is_multiple_of(WORD_BYTES), + "quotient memory pointer {offset:#x} is not 32-byte word aligned" + ); + QuotientMem::Literal(offset) } Value::Identifier(name, offset) => { - assert!(offset >= 0, "negative quotient memory token offset"); + let offset = u32::try_from(offset).expect( + "quotient memory token offset must be a non-negative offset that fits in u32", + ); + assert!( + (offset as usize).is_multiple_of(WORD_BYTES), + "quotient memory token offset {offset:#x} is not 32-byte word aligned" + ); let token = quotient_mem_token_from_name(name) .unwrap_or_else(|| panic!("unsupported quotient memory token: {name}")); if offset == 0 { QuotientMem::Token(token) } else { - QuotientMem::TokenOffset(token, offset as u32) + QuotientMem::TokenOffset(token, offset) } } } @@ -2654,7 +3235,7 @@ pub(crate) fn quotient_pow5_base(expr: &QuotientExpr) -> Option<&QuotientExpr> { /// Extract one limb shape from a larger affine sum and return the residue. pub(crate) fn quotient_limb_subshape( expr: &QuotientExpr, -) -> Option<(QuotientLimbShape, QuotientExpr)> { +) -> Option<(QuotientLimbShape, QuotientExpr, Vec)> { let mut terms = Vec::new(); let mut constant = Fq::ZERO; if !collect_quotient_affine_terms(expr, Fq::ONE, &mut terms, &mut constant) { @@ -2672,15 +3253,23 @@ pub(crate) fn quotient_limb_subshape( return None; } + // Split the affine terms into the residue (unused terms plus the constant) + // and the matched terms that reconstruct `shape` as a plain sum. The matched + // terms are the panic-free fallback for `emit_limb_shape`: their sum equals + // the fused opcode's value, but each is a single scaled load/product that + // uses u16-capable constant loads. let used = used.into_iter().collect::>(); let mut residue = QuotientExpr::Const(quotient_fq_to_u256(constant)); + let mut matched = Vec::with_capacity(used.len()); for (idx, (coeff, term)) in terms.into_iter().enumerate() { + let scaled = quotient_scaled_term_expr(coeff, (*term).clone()); if used.contains(&idx) { - continue; + matched.push(scaled); + } else { + residue = quotient_sum_expr(residue, scaled); } - residue = quotient_sum_expr(residue, quotient_scaled_term_expr(coeff, (*term).clone())); } - Some((shape, residue)) + Some((shape, residue, matched)) } /// Recognize a whole affine foreign-field/ECC identity that can be evaluated diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/reference.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/reference.rs new file mode 100644 index 000000000..baf4eaf22 --- /dev/null +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/vm/reference.rs @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: CC0-1.0 +//! Independent reference interpreter for finalized quotient VM bytecode. +//! +//! This is the second implementation of the quotient VM ABI. It exists so the +//! generator can certify its own output: for every render, the emitted bytecode +//! is executed here and compared against direct evaluation of the +//! [`QuotientExpr`] trees the bytecode was lowered from. A disagreement means +//! the emitter, one of its shape recognizers, or the run-compaction pass +//! miscompiled an identity, and the render is rejected before any artifact is +//! produced. +//! +//! Why a random-assignment check is sufficient: the certifier samples a +//! deterministic challenge from the finalized quotient bytecode, constant +//! table, oracle expression trees, and VK payload, then evaluates the fixed +//! program at that assignment. Any miscompilation therefore yields a wrong +//! polynomial that was fixed before the challenge was known, and it disagrees +//! with the correct one with probability `1 - deg/|Fr|`. One challenge-derived +//! evaluation is enough to catch it with overwhelming probability. +//! +//! Scope. This certifies the **emitter to reference-interpreter** leg, which is +//! where the shape recognizers in the parent module live. The +//! **reference-interpreter to Yul** leg is covered separately by the opcode and +//! memory-token table conformance tests and by the per-identity Rust/Solidity +//! trace differential on fixture circuits. Identities executed as inline Yul, +//! native callbacks, or the structured tail are not lowered to bytecode at all, +//! so this module evaluates them directly from their expression trees and they +//! remain covered only by those other two mechanisms. + +use std::collections::HashMap; + +use ff::{Field, PrimeField}; +use midnight_curves::Fq; +use ruint::aliases::U256; +use sha3::{Digest, Keccak256}; + +use super::{ + QuotientExpr, QuotientMem, QUOTIENT_VM_LIMBS, QUOTIENT_VM_PAIRWISE_COEFFS, + Q_MODARITH7_FLAG_COND, Q_MODARITH7_FLAG_CONST, Q_OP_ADD, Q_OP_ADD_CONST, Q_OP_ADD_CONST_U8, + Q_OP_ADD_MEM_U16, Q_OP_ADD_MUL_CONST_U8_MEM_U16, Q_OP_ADD_MUL_MEM_MEM, + Q_OP_ADD_MUL_MEM_MEM_CONST_U8, Q_OP_AFFINE_SUM, Q_OP_BILIN7_PAIRWISE, Q_OP_BILIN7_ROW, + Q_OP_FOLD_MAIN, Q_OP_FOLD_SELECTOR, Q_OP_LIN7, Q_OP_MODARITH7, Q_OP_MUL, Q_OP_MUL_CONST, + Q_OP_MUL_CONST_U8, Q_OP_MUL_MEM_U16, Q_OP_NATIVE_IDENTITY, Q_OP_NATIVE_LOOKUP, + Q_OP_NATIVE_PERMUTATION, Q_OP_NEG, Q_OP_POW5, Q_OP_PUSH_CONST, Q_OP_PUSH_CONST_U8, + Q_OP_PUSH_MEM_LITERAL, Q_OP_PUSH_MEM_TOKEN, Q_OP_PUSH_MEM_TOKEN_OFFSET, Q_OP_PUSH_MEM_U16, + Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16, Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8, +}; +use crate::lowering::layout::WORD_BYTES; + +/// Deterministic pseudorandom assignment for every verifier memory slot. +/// +/// The bytecode addresses memory by absolute pointer or by symbolic token, and +/// the expression trees address the exact same slots. Deriving each value from +/// its own address means both sides observe identical memory without the +/// certifier having to enumerate the live address set up front, which in turn +/// means a pointer-packing bug shows up as a value mismatch rather than as a +/// missing map key. +#[derive(Clone, Debug)] +pub(crate) struct QuotientRefMemory { + seed: [u8; 32], + cache: HashMap<(u8, u32), Fq>, +} + +impl QuotientRefMemory { + /// Build an assignment for one certification run. + pub(crate) fn new(seed: [u8; 32]) -> Self { + Self { + seed, + cache: HashMap::new(), + } + } + + /// Value at an absolute memory pointer. + pub(crate) fn literal(&mut self, ptr: u32) -> Fq { + self.derive(0, ptr) + } + + /// Value behind a symbolic memory token, optionally offset. + pub(crate) fn token(&mut self, token: u8, offset: u32) -> Fq { + // Tokens resolve to generated addresses disjoint from the literal + // pointer space, so they get their own domain tag. + self.derive(1 + token, offset) + } + + /// Derive one field element from a domain-separated address. + fn derive(&mut self, domain: u8, address: u32) -> Fq { + if let Some(value) = self.cache.get(&(domain, address)) { + return *value; + } + let mut hasher = Keccak256::new(); + hasher.update(b"midfall/quotient-vm/reference-memory/v1"); + hasher.update(self.seed); + hasher.update([domain]); + hasher.update(address.to_be_bytes()); + let lo = hasher.finalize(); + + let mut hasher = Keccak256::new(); + hasher.update(b"midfall/quotient-vm/reference-memory/v1/hi"); + hasher.update(lo); + let hi = hasher.finalize(); + + let mut wide = [0u8; 64]; + wide[..32].copy_from_slice(&lo); + wide[32..].copy_from_slice(&hi); + let value = >::from_uniform_bytes(&wide); + self.cache.insert((domain, address), value); + value + } +} + +/// Evaluate a typed quotient expression directly, without going through the VM. +/// +/// This is the oracle side of the certification: it follows the same shape as +/// `Expression::evaluate` in the native verifier and knows nothing about +/// opcodes, shape recognizers, or operand packing. +pub(crate) fn eval_quotient_expr(expr: &QuotientExpr, mem: &mut QuotientRefMemory) -> Fq { + match expr { + QuotientExpr::Const(value) => fq_from_u256(*value), + QuotientExpr::Mem(QuotientMem::Literal(ptr)) => mem.literal(*ptr), + QuotientExpr::Mem(QuotientMem::Token(token)) => mem.token(*token, 0), + QuotientExpr::Mem(QuotientMem::TokenOffset(token, offset)) => mem.token(*token, *offset), + QuotientExpr::Add(lhs, rhs) => eval_quotient_expr(lhs, mem) + eval_quotient_expr(rhs, mem), + QuotientExpr::Mul(lhs, rhs) => eval_quotient_expr(lhs, mem) * eval_quotient_expr(rhs, mem), + QuotientExpr::Neg(inner) => -eval_quotient_expr(inner, mem), + } +} + +/// Evaluate one identity expression subprogram, up to but excluding its fold. +/// +/// Returns an error rather than panicking: this runs inside the generator, and +/// a malformed stream must surface as a `GeneratorError`, not a process abort. +pub(crate) fn eval_quotient_identity( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, +) -> Result { + let mut stack: Vec = Vec::new(); + let mut idx = 0usize; + + // Pop helpers keep the error path uniform; the offline validator has + // already proven depth safety, so these only fire on validator drift. + macro_rules! pop { + () => { + stack + .pop() + .ok_or_else(|| format!("reference VM stack underflow at byte {idx}"))? + }; + } + + while idx < bytes.len() { + let op = bytes[idx]; + match op { + Q_OP_PUSH_CONST => { + let slot = read_u16(bytes, idx + 1)? as usize; + stack.push(const_at(consts, slot, idx)?); + idx += 3; + } + Q_OP_PUSH_CONST_U8 => { + let slot = byte_at(bytes, idx + 1)? as usize; + stack.push(const_at(consts, slot, idx)?); + idx += 2; + } + Q_OP_PUSH_MEM_LITERAL => { + let ptr = read_u32(bytes, idx + 1)?; + stack.push(mem.literal(ptr)); + idx += 5; + } + Q_OP_PUSH_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1)? as u32; + stack.push(mem.literal(ptr)); + idx += 3; + } + Q_OP_PUSH_MEM_TOKEN => { + let token = byte_at(bytes, idx + 1)?; + stack.push(mem.token(token, 0)); + idx += 2; + } + Q_OP_PUSH_MEM_TOKEN_OFFSET => { + let token = byte_at(bytes, idx + 1)?; + let offset = read_u32(bytes, idx + 2)?; + stack.push(mem.token(token, offset)); + idx += 6; + } + Q_OP_ADD => { + let rhs = pop!(); + let lhs = pop!(); + stack.push(lhs + rhs); + idx += 1; + } + Q_OP_MUL => { + let rhs = pop!(); + let lhs = pop!(); + stack.push(lhs * rhs); + idx += 1; + } + Q_OP_NEG => { + let value = pop!(); + stack.push(-value); + idx += 1; + } + Q_OP_POW5 => { + let value = pop!(); + let squared = value * value; + stack.push(value * squared * squared); + idx += 1; + } + Q_OP_ADD_CONST_U8 | Q_OP_MUL_CONST_U8 => { + let slot = byte_at(bytes, idx + 1)? as usize; + let value = const_at(consts, slot, idx)?; + let acc = pop!(); + stack.push(if op == Q_OP_ADD_CONST_U8 { + acc + value + } else { + acc * value + }); + idx += 2; + } + Q_OP_ADD_CONST | Q_OP_MUL_CONST => { + let slot = read_u16(bytes, idx + 1)? as usize; + let value = const_at(consts, slot, idx)?; + let acc = pop!(); + stack.push(if op == Q_OP_ADD_CONST { + acc + value + } else { + acc * value + }); + idx += 3; + } + Q_OP_ADD_MEM_U16 | Q_OP_MUL_MEM_U16 => { + let ptr = read_u16(bytes, idx + 1)? as u32; + let value = mem.literal(ptr); + let acc = pop!(); + stack.push(if op == Q_OP_ADD_MEM_U16 { + acc + value + } else { + acc * value + }); + idx += 3; + } + Q_OP_ADD_MUL_MEM_MEM_CONST_U8 => { + let acc = pop!(); + let (term, len) = affine_product_term(bytes, consts, mem, idx + 1)?; + stack.push(acc + term); + idx += 1 + len; + } + Q_OP_ADD_MUL_CONST_U8_MEM_U16 => { + let acc = pop!(); + let (term, len) = affine_linear_term(bytes, consts, mem, idx + 1)?; + stack.push(acc + term); + idx += 1 + len; + } + Q_OP_ADD_MUL_MEM_MEM => { + let lhs = read_u16(bytes, idx + 1)? as u32; + let rhs = read_u16(bytes, idx + 3)? as u32; + let acc = pop!(); + stack.push(acc + mem.literal(lhs) * mem.literal(rhs)); + idx += 5; + } + Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 | Q_OP_RUN_ADD_MUL_CONST_U8_MEM_U16 => { + // Run compaction is a pure encoding change: the same terms in + // the same order, with one shared count instead of repeated + // opcode bytes. + let count = read_u16(bytes, idx + 1)? as usize; + if count == 0 { + return Err(format!("reference VM zero-length run at byte {idx}")); + } + let mut cursor = idx + 3; + let mut acc = pop!(); + for _ in 0..count { + let (term, len) = if op == Q_OP_RUN_ADD_MUL_MEM_MEM_CONST_U8 { + affine_product_term(bytes, consts, mem, cursor)? + } else { + affine_linear_term(bytes, consts, mem, cursor)? + }; + acc += term; + cursor += len; + } + stack.push(acc); + idx = cursor; + } + Q_OP_AFFINE_SUM => { + // Mixed run: all linear terms first, then all product terms, + // matching `compact_quotient_runs`. + let lin_count = read_u16(bytes, idx + 1)? as usize; + let product_count = read_u16(bytes, idx + 3)? as usize; + if lin_count == 0 || product_count == 0 { + return Err(format!( + "reference VM AFFINE_SUM at byte {idx} requires nonzero counts" + )); + } + let mut cursor = idx + 5; + let mut acc = pop!(); + for _ in 0..lin_count { + let (term, len) = affine_linear_term(bytes, consts, mem, cursor)?; + acc += term; + cursor += len; + } + for _ in 0..product_count { + let (term, len) = affine_product_term(bytes, consts, mem, cursor)?; + acc += term; + cursor += len; + } + stack.push(acc); + idx = cursor; + } + Q_OP_LIN7 => { + let (value, len) = limb_linear_form(bytes, consts, mem, idx + 1)?; + stack.push(value); + idx += 1 + len; + } + Q_OP_BILIN7_ROW => { + let (value, len) = limb_row_form(bytes, consts, mem, idx + 1)?; + stack.push(value); + idx += 1 + len; + } + Q_OP_BILIN7_PAIRWISE => { + let (value, len) = limb_pairwise_form(bytes, consts, mem, idx + 1)?; + stack.push(value); + idx += 1 + len; + } + Q_OP_MODARITH7 => { + let (value, len) = modarith7_form(bytes, consts, mem, idx + 1)?; + stack.push(value); + idx += 1 + len; + } + Q_OP_FOLD_MAIN + | Q_OP_FOLD_SELECTOR + | Q_OP_NATIVE_PERMUTATION + | Q_OP_NATIVE_LOOKUP + | Q_OP_NATIVE_IDENTITY => { + return Err(format!( + "reference VM found stream opcode {op:#x} inside an identity expression at byte {idx}" + )); + } + _ => { + return Err(format!("reference VM unknown opcode {op:#x} at byte {idx}")); + } + } + } + + if stack.len() != 1 { + return Err(format!( + "reference VM identity left {} value(s) on the stack, expected 1", + stack.len() + )); + } + Ok(stack.pop().expect("checked length")) +} + +/// Decode one `const * mload(ptr)` term and return its byte length. +fn affine_linear_term( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let ptr = read_u16(bytes, idx)? as u32; + let slot = byte_at(bytes, idx + 2)? as usize; + Ok((const_at(consts, slot, idx)? * mem.literal(ptr), 3)) +} + +/// Decode one `mload(lhs) * mload(rhs) * const` term and return its byte +/// length. +fn affine_product_term( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let lhs = read_u16(bytes, idx)? as u32; + let rhs = read_u16(bytes, idx + 2)? as u32; + let slot = byte_at(bytes, idx + 4)? as usize; + Ok(( + mem.literal(lhs) * mem.literal(rhs) * const_at(consts, slot, idx)?, + 5, + )) +} + +/// Decode a seven-limb linear form `sum_i const_i * mload(ptr_i)`. +fn limb_linear_form( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let mut acc = Fq::ZERO; + let mut cursor = idx; + for _ in 0..QUOTIENT_VM_LIMBS { + let slot = byte_at(bytes, cursor)? as usize; + let ptr = read_u16(bytes, cursor + 1)? as u32; + acc += const_at(consts, slot, cursor)? * mem.literal(ptr); + cursor += 3; + } + Ok((acc, cursor - idx)) +} + +/// Decode `mload(lhs) * sum_i const_i * mload(rhs_i)`. +fn limb_row_form( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let lhs = read_u16(bytes, idx)? as u32; + let lhs_value = mem.literal(lhs); + let (inner, len) = limb_linear_form(bytes, consts, mem, idx + 2)?; + Ok((lhs_value * inner, 2 + len)) +} + +/// Decode a 7x7 pairwise product with `i + j` indexed coefficients. +fn limb_pairwise_form( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let lhs_base = read_u16(bytes, idx)? as u32; + let rhs_base = read_u16(bytes, idx + 2)? as u32; + let coeff_idx = idx + 4; + // Bounds-check the whole coefficient block once so the inner loop cannot + // read past the program. + byte_at(bytes, coeff_idx + QUOTIENT_VM_PAIRWISE_COEFFS - 1)?; + + let mut acc = Fq::ZERO; + for i in 0..QUOTIENT_VM_LIMBS { + let lhs = mem.literal(lhs_base + (i as u32) * WORD_BYTES as u32); + for j in 0..QUOTIENT_VM_LIMBS { + let rhs = mem.literal(rhs_base + (j as u32) * WORD_BYTES as u32); + let slot = bytes[coeff_idx + i + j] as usize; + acc += lhs * rhs * const_at(consts, slot, coeff_idx)?; + } + } + Ok((acc, 4 + QUOTIENT_VM_PAIRWISE_COEFFS)) +} + +/// Decode the dynamic mixed seven-limb affine form. +fn modarith7_form( + bytes: &[u8], + consts: &[U256], + mem: &mut QuotientRefMemory, + idx: usize, +) -> Result<(Fq, usize), String> { + let mut cursor = idx; + let flags = byte_at(bytes, cursor)?; + if flags & !(Q_MODARITH7_FLAG_COND | Q_MODARITH7_FLAG_CONST) != 0 { + return Err(format!( + "reference VM MODARITH7 unknown flag bits {flags:#x} at byte {idx}" + )); + } + cursor += 1; + + let cond = if flags & Q_MODARITH7_FLAG_COND != 0 { + let ptr = read_u16(bytes, cursor)? as u32; + cursor += 2; + Some(ptr) + } else { + None + }; + + let mut acc = Fq::ZERO; + if flags & Q_MODARITH7_FLAG_CONST != 0 { + let slot = byte_at(bytes, cursor)? as usize; + acc += const_at(consts, slot, cursor)?; + cursor += 1; + } + + let lin_count = byte_at(bytes, cursor)? as usize; + let row_count = byte_at(bytes, cursor + 1)? as usize; + let pairwise_count = byte_at(bytes, cursor + 2)? as usize; + let mem_count = byte_at(bytes, cursor + 3)? as usize; + let product_count = byte_at(bytes, cursor + 4)? as usize; + cursor += 5; + + for _ in 0..lin_count { + let (value, len) = limb_linear_form(bytes, consts, mem, cursor)?; + acc += value; + cursor += len; + } + for _ in 0..row_count { + let (value, len) = limb_row_form(bytes, consts, mem, cursor)?; + acc += value; + cursor += len; + } + for _ in 0..pairwise_count { + let (value, len) = limb_pairwise_form(bytes, consts, mem, cursor)?; + acc += value; + cursor += len; + } + for _ in 0..mem_count { + // Note the operand order here is const-slot first, unlike the standalone + // `ADD_MUL_CONST_U8_MEM_U16` term, so this cannot reuse the helper. + let slot = byte_at(bytes, cursor)? as usize; + let ptr = read_u16(bytes, cursor + 1)? as u32; + acc += const_at(consts, slot, cursor)? * mem.literal(ptr); + cursor += 3; + } + for _ in 0..product_count { + let slot = byte_at(bytes, cursor)? as usize; + let lhs = read_u16(bytes, cursor + 1)? as u32; + let rhs = read_u16(bytes, cursor + 3)? as u32; + acc += const_at(consts, slot, cursor)? * mem.literal(lhs) * mem.literal(rhs); + cursor += 5; + } + + if let Some(cond) = cond { + acc *= mem.literal(cond); + } + Ok((acc, cursor - idx)) +} + +/// Read one byte with an explicit bounds error. +fn byte_at(bytes: &[u8], idx: usize) -> Result { + bytes + .get(idx) + .copied() + .ok_or_else(|| format!("reference VM read past end of program at byte {idx}")) +} + +/// Read a big-endian `u16` operand. +fn read_u16(bytes: &[u8], idx: usize) -> Result { + Ok(u16::from_be_bytes([ + byte_at(bytes, idx)?, + byte_at(bytes, idx + 1)?, + ])) +} + +/// Read a big-endian `u32` operand. +fn read_u32(bytes: &[u8], idx: usize) -> Result { + Ok(u32::from_be_bytes([ + byte_at(bytes, idx)?, + byte_at(bytes, idx + 1)?, + byte_at(bytes, idx + 2)?, + byte_at(bytes, idx + 3)?, + ])) +} + +/// Look up a constant-table slot with an explicit bounds error. +fn const_at(consts: &[U256], slot: usize, idx: usize) -> Result { + consts + .get(slot) + .copied() + .map(fq_from_u256) + .ok_or_else(|| format!("reference VM constant slot {slot} out of range at byte {idx}")) +} + +/// Convert a canonical `U256` constant-table entry into Fr. +fn fq_from_u256(value: U256) -> Fq { + let bytes = value.to_le_bytes::<32>(); + let repr = ::Repr::from(bytes); + Option::::from(Fq::from_repr(repr)).expect("constant table holds canonical field elements") +} diff --git a/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs b/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs index bc8242de0..1b68231be 100644 --- a/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs +++ b/proofs/solidity-verifier/src/lowering/quotient_numerator/yul_emit.rs @@ -456,6 +456,16 @@ impl<'a> Evaluator<'a> { // `(Vec, String)` entries. let selector_expr = chunked.selector_expression(); + // Fail closed on a chunk/helper-eval count mismatch: zip would + // otherwise silently drop the excess chunks, removing helper + // constraints from the numerator. + assert_eq!( + chunked.input_expression_chunks().len(), + h_evals.len(), + "lookup {lookup_idx}: input chunk count {} != helper eval count {}", + chunked.input_expression_chunks().len(), + h_evals.len(), + ); for (input_chunk, h_eval) in chunked.input_expression_chunks().iter().zip(h_evals.iter()) { @@ -479,10 +489,17 @@ impl<'a> Evaluator<'a> { let k = f_plus_beta_vars.len(); if k == 0 { - // Empty chunk shouldn't happen but emit a no-op. - let zero = self.fresh_var(); - lines.push(format!("let {zero} := 0")); - out.push((lines, zero)); + // Unreachable today (BatchedArgument::new requires >= 1 + // parallel lookup and slice::chunks never yields an empty + // chunk), but emit the reference-faithful value rather than + // 0 so a future chunking change cannot silently drop the + // constraint. For an empty chunk the native verifier + // (plonk/logup.rs) computes helper_eval * (empty product = 1) + // - (empty sum = 0) = helper_eval, enforcing h == 0. Emitting + // 0 would leave h unconstrained while the accumulator still + // folds this h_eval into sum_h, letting a prover forge lookup + // balance. + out.push((lines, h_eval.to_string())); continue; } @@ -571,11 +588,18 @@ impl<'a> Evaluator<'a> { let beta = self.fresh_var(); lines.push(format!("let {beta} := mload(BETA_MPTR)")); - // Σ_h h_eval[c] + // Σ_h h_eval[c]. Empty for a lookup with no input expressions + // (zero helper chunks); the native verifier's sum_helpers folds + // over an empty set to 0, so mirror that instead of indexing + // h_evals[0] out of bounds and panicking at codegen. let sum_h = self.fresh_var(); - lines.push(format!("let {sum_h} := {}", h_evals[0])); - for h in &h_evals[1..] { - lines.push(format!("{sum_h} := addmod({sum_h}, {h}, r)")); + if let Some((first, rest)) = h_evals.split_first() { + lines.push(format!("let {sum_h} := {first}")); + for h in rest { + lines.push(format!("{sum_h} := addmod({sum_h}, {h}, r)")); + } + } else { + lines.push(format!("let {sum_h} := 0")); } // selector eval (full Expression; not necessarily a @@ -788,6 +812,14 @@ impl<'a> Evaluator<'a> { .get(&(column_index, query.rotation().0)) .copied()? } else { + // The non-committed public-input column only has its local + // Rotation::cur() interpolation at INSTANCE_EVAL_MPTR. + // Decline to treat a rotated query as a direct memory + // pointer; the constructor rejects rotated instance queries + // and instance_eval_at hard-asserts rotation == 0. + if query.rotation().0 != 0 { + return None; + } self.data.instance_eval } } @@ -1103,8 +1135,18 @@ impl<'a> Evaluator<'a> { .to_string() } else { // The current public API supports one non-committed instance - // column, whose Lagrange-combined evaluation is computed by - // the template prologue and stored at INSTANCE_EVAL_MPTR. + // column, whose Lagrange-combined evaluation is computed by the + // template prologue and stored at INSTANCE_EVAL_MPTR for + // Rotation::cur() only. Hard-assert (not debug_assert) so a + // rotated query that ever bypasses the far-away constructor guard + // (builder/api.rs) fails closed in release builds too, instead of + // silently evaluating instance(x) in place of instance(x*omega^k) + // and generating a verifier that checks a different identity than + // the native Midfall verifier. + assert_eq!( + rotation, 0, + "rotated public instance query reached Yul quotient emission" + ); self.data.instance_eval.to_string() } } @@ -1141,7 +1183,7 @@ fn u256_string(value: U256) -> String { /// Stable variable name for a column evaluation and rotation. fn column_eval_var(prefix: &'static str, column_index: usize, rotation: i32) -> String { match rotation.cmp(&0) { - Ordering::Less => format!("{prefix}_{column_index}_prev_{}", rotation.abs()), + Ordering::Less => format!("{prefix}_{column_index}_prev_{}", rotation.unsigned_abs()), Ordering::Equal => format!("{prefix}_{column_index}"), Ordering::Greater => format!("{prefix}_{column_index}_next_{rotation}"), } diff --git a/proofs/solidity-verifier/src/lowering/render/models.rs b/proofs/solidity-verifier/src/lowering/render/models.rs index 0ff85f228..f5dbdafa4 100644 --- a/proofs/solidity-verifier/src/lowering/render/models.rs +++ b/proofs/solidity-verifier/src/lowering/render/models.rs @@ -10,6 +10,8 @@ use std::fmt; use askama::{Error, Template}; +use group::{prime::PrimeCurveAffine, Curve, Group}; +use midnight_curves::{G1Affine, G1Projective}; use ruint::aliases::U256; use crate::lowering::{ @@ -39,6 +41,8 @@ pub(crate) struct TemplateConstants { pub(crate) pairing_two_pair_bytes: usize, /// EIP-2537 precompile constants. pub(crate) eip2537: Eip2537TemplateConstants, + /// Exact EIP-2537/EIP-2565 gas bounds forwarded to precompile calls. + pub(crate) gas: GasTemplateConstants, /// EIP-198 modexp constants. pub(crate) modexp: ModexpTemplateConstants, /// Public accumulator layout constants. @@ -54,6 +58,32 @@ pub(crate) struct Eip2537TemplateConstants { pub(crate) g1msm_address: usize, pub(crate) pairing_address: usize, pub(crate) smoke_scratch_bytes: usize, + /// BLS12-381 G1 generator in EIP-2537 padded encoding. + /// + /// Used with [`Self::g1_double_generator`] as a known-answer vector for the + /// constructor smoke test: identity-only probes are satisfied by + /// implementations that never do any real curve arithmetic. + pub(crate) g1_generator: G1Words, + /// Twice the BLS12-381 G1 generator, in EIP-2537 padded encoding. + pub(crate) g1_double_generator: G1Words, +} + +/// Exact precompile gas bounds rendered into templates. +/// +/// These are the EIP-2537/EIP-2565 scheduled costs, forwarded verbatim so a +/// failing precompile call burns at most its scheduled cost instead of the +/// full 63/64 of the transaction budget. See [`layout::gas`] for the model +/// and the upward-repricing liveness caveat. +#[derive(Clone, Copy, Debug)] +pub(crate) struct GasTemplateConstants { + /// EIP-2537 G1ADD flat cost. + pub(crate) g1add: u64, + /// EIP-2537 G1MSM cost for a single (point, scalar) pair. + pub(crate) g1msm_one_pair: u64, + /// EIP-2537 pairing cost for the verifier's two-pair check. + pub(crate) pairing_two_pair: u64, + /// EIP-2565 modexp cost for the 32-byte base/exp/mod frame. + pub(crate) modexp: u64, } /// EIP-198 modexp frame constants rendered into templates. @@ -75,6 +105,7 @@ pub(crate) struct ModexpTemplateConstants { pub(crate) struct AccumulatorTemplateConstants { pub(crate) limbs_per_word: usize, pub(crate) pairing_batch_domain_tag_hex: &'static str, + pub(crate) pairing_batch_vk_digest_offset: usize, pub(crate) pairing_batch_rhs_offset: usize, pub(crate) pairing_batch_lhs_offset: usize, pub(crate) pairing_batch_acc_rhs_offset: usize, @@ -142,6 +173,12 @@ pub(crate) struct QuotientVmTemplateConstants { pub(crate) limb_pairwise_coeffs: usize, } +/// Convert a G1 point into the tuple form the templates render. +fn g1_words(point: G1Affine) -> G1Words { + let [x_hi, x_lo, y_hi, y_lo] = crate::lowering::encoding::g1_to_u256s(point); + (x_hi, x_lo, y_hi, y_lo) +} + impl Default for TemplateConstants { /// Build template constants from the Rust-side layout and VM specs. fn default() -> Self { @@ -158,6 +195,17 @@ impl Default for TemplateConstants { g1msm_address: layout::precompile::G1MSM_ADDRESS, pairing_address: layout::precompile::PAIRING_ADDRESS, smoke_scratch_bytes: layout::PAIRING_TWO_PAIR_BYTES, + g1_generator: g1_words(G1Affine::generator()), + g1_double_generator: { + let g = G1Projective::generator(); + g1_words((g + g).to_affine()) + }, + }, + gas: GasTemplateConstants { + g1add: layout::gas::G1ADD_GAS, + g1msm_one_pair: layout::gas::g1msm_gas(1), + pairing_two_pair: layout::gas::pairing_gas(2), + modexp: layout::gas::modexp_gas_word_frame(), }, modexp: ModexpTemplateConstants { address: layout::precompile::MODEXP_ADDRESS, @@ -173,6 +221,7 @@ impl Default for TemplateConstants { accumulator: AccumulatorTemplateConstants { limbs_per_word: layout::accumulator::LIMBS_PER_WORD, pairing_batch_domain_tag_hex: layout::accumulator::PAIRING_BATCH_DOMAIN_TAG_HEX, + pairing_batch_vk_digest_offset: layout::accumulator::PAIRING_BATCH_VK_DIGEST_OFFSET, pairing_batch_rhs_offset: layout::accumulator::PAIRING_BATCH_RHS_OFFSET, pairing_batch_lhs_offset: layout::accumulator::PAIRING_BATCH_LHS_OFFSET, pairing_batch_acc_rhs_offset: layout::accumulator::PAIRING_BATCH_ACC_RHS_OFFSET, @@ -257,6 +306,10 @@ pub(crate) struct Halo2VerifyingKey { pub(crate) const VK_RUNTIME_PREFIX: u8 = 0xfe; /// Number of bytes skipped before copying the separate VK payload. pub(crate) const VK_RUNTIME_PREFIX_LEN: usize = 1; +/// EIP-170 deployed-contract runtime code-size limit (24576 bytes). The VK is +/// shipped as its own data contract whose runtime is `[INVALID, ...payload]`, +/// so it must fit under this bound to be deployable on EIP-170 chains. +pub(crate) const EIP_170_MAX_RUNTIME_BYTES: usize = 0x6000; impl Halo2VerifyingKey { /// Reconstruct and validate the typed VK payload layout. @@ -306,7 +359,15 @@ impl Halo2VerifyingKey { self.len() )); } - let constructor_memory = VkConstructorMemoryLayout::new(self.runtime_len()); + let runtime_len = self.runtime_len(); + if runtime_len > EIP_170_MAX_RUNTIME_BYTES { + return Err(format!( + "VK runtime code size {runtime_len} bytes exceeds the EIP-170 limit of \ + {EIP_170_MAX_RUNTIME_BYTES} bytes; the VK data contract would revert at \ + deployment. Reduce the circuit's constant/commitment count." + )); + } + let constructor_memory = VkConstructorMemoryLayout::new(runtime_len); constructor_memory.validate()?; if self.constructor_payload_mptr != constructor_memory.payload_mptr { return Err(format!( @@ -424,11 +485,24 @@ pub(crate) struct Halo2Verifier { pub(crate) quotient_wide_limb7_helper: bool, /// Largest generated G1MSM input length, smoke-tested at deployment. pub(crate) constructor_g1msm_smoke_input_bytes: usize, + /// Exact EIP-2537 G1MSM cost for the deployment smoke probe over + /// [`Self::constructor_g1msm_smoke_input_bytes`]. + pub(crate) constructor_g1msm_smoke_gas: u64, + /// Exact EIP-2537 G1MSM cost bound for the accumulator RHS MSM, sized + /// for its worst case: the carried RHS point plus every generated + /// fixed-base tail scalar nonzero. Zero tail scalars are omitted at + /// runtime, which only lowers the actual cost below this bound. + /// Zero when the verifier has no public accumulator. + pub(crate) acc_rhs_msm_gas: u64, pub(crate) limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], pub(crate) wide_limb7_yul_coeffs: [&'static str; layout::quotient_limb::LIN_COEFFS], pub(crate) fr_delta: String, pub(crate) embedded_vk: Option, pub(crate) expected_vk_codehash: Option, + /// keccak over the build's identity components (P10/L-8): feature + /// profile, vk_digest, VK codehash-or-zero, SRS fingerprint, optional + /// deployment provenance tag. Emitted as the public BUILD_ID constant. + pub(crate) build_id: U256, pub(crate) vk_len: usize, /// Generated public-instance count for this pinned VK/proof layout. pub(crate) num_instances: usize, @@ -632,8 +706,32 @@ pub(crate) struct QuotientProgram { pub(crate) selector_tail_updates: Vec, /// Operand stack / callback scratch base. pub(crate) stack_mptr: usize, + /// First address past the operand stack / callback scratch region. + /// + /// MF-3: spill sites clamp `q_sp` against this so a malformed program + /// cannot walk the stack pointer out of its registered region. + pub(crate) stack_hi: usize, + /// Number of Fr words in the generated quotient constant table. + /// + /// MF-3: constant-table indexes decoded from program bytes are clamped + /// against this so an out-of-range index cannot read program bytes (or + /// commitment words) as field constants. + pub(crate) num_consts: usize, /// Memory pointer to the first encoded program word. pub(crate) program_mptr: usize, + /// Lowest word address a VM memory operand may load from (P12/L-6). + /// + /// Coarse `[operand_lo, operand_hi]` clamp over the union of the plan's + /// quotient read windows, rendered into the interpreter's operand-decode + /// arms. Build-time `validate_quotient_mem_ptrs` still enforces exact + /// per-window membership; the runtime clamp is defence in depth for a + /// program trusted only through the VK codehash pin. + pub(crate) operand_lo: usize, + /// Highest word address a VM memory operand may load from (inclusive). + pub(crate) operand_hi: usize, + /// Number of simple-selector accumulator buckets addressable by + /// FOLD_SELECTOR's index operand (P12/L-6). + pub(crate) num_selector_buckets: usize, } #[derive(Clone, Copy, Debug)] @@ -1032,7 +1130,7 @@ mod tests { let memory = VerifierMemoryLayout::new( &ConstraintSystemMeta::default(), &synthetic_vk(0, 0), - Ptr::memory(0x1000), + Ptr::memory(0x2000), VerifierMemoryLayoutConfig::default(), ); let mut proof = ProofReadPlan::default(); @@ -1072,11 +1170,14 @@ mod tests { quotient_limb7_helper: false, quotient_wide_limb7_helper: false, constructor_g1msm_smoke_input_bytes: crate::lowering::layout::G1_MSM_PAIR_BYTES, + constructor_g1msm_smoke_gas: crate::lowering::layout::gas::g1msm_gas(1), + acc_rhs_msm_gas: 0, limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::LIMB7_YUL_COEFFS, wide_limb7_yul_coeffs: crate::lowering::quotient_numerator::vm::WIDE_LIMB7_YUL_COEFFS, fr_delta: crate::lowering::quotient_numerator::vm::fr_delta_literal(), embedded_vk: None, expected_vk_codehash: Some(U256::from(1u64)), + build_id: U256::ZERO, vk_len: 0, num_instances: 1, k: 8, @@ -1086,9 +1187,9 @@ mod tests { }, memory, vk_header: Default::default(), - vk_mptr: Ptr::memory(0x1000), - challenge_mptr: Ptr::memory(0x1200), - theta_mptr: Ptr::memory(0x1300), + vk_mptr: Ptr::memory(0x2000), + challenge_mptr: Ptr::memory(0x2200), + theta_mptr: Ptr::memory(0x2300), proof_cptr: Ptr::calldata(proof_cptr), abi_selector_bytes: crate::lowering::layout::abi::SELECTOR_BYTES, abi_proof_head_offset: crate::lowering::layout::abi::VERIFY_PROOF_PROOF_HEAD_OFFSET, @@ -1128,7 +1229,14 @@ mod tests { selector_max_power: 0, selector_tail_updates: vec![], stack_mptr: 0, + stack_hi: usize::MAX, + num_consts: usize::MAX, program_mptr: 0, + // Synthetic model: a permissive clamp window keeps the + // rendered guards inert for layout-shape tests. + operand_lo: 0, + operand_hi: usize::MAX, + num_selector_buckets: 0, }), pcs_computations: vec![], simple_selector_cols: vec![], diff --git a/proofs/solidity-verifier/src/lowering/tests.rs b/proofs/solidity-verifier/src/lowering/tests.rs index d3ea64048..60b6998d3 100644 --- a/proofs/solidity-verifier/src/lowering/tests.rs +++ b/proofs/solidity-verifier/src/lowering/tests.rs @@ -38,7 +38,7 @@ use super::{ use crate::{ api::{ AccumulatorEncoding, CommittedInstanceCommitmentKind, GeneratorConfig, GeneratorError, - QuotientIdentityManifestTarget, QuotientIdentitySource, + QuotientIdentityManifestTarget, QuotientIdentitySource, RenderOptions, }, SolidityGenerator, }; @@ -278,6 +278,277 @@ impl Circuit for LoweringPlanTestCircuit { } } +/// Number of advice columns backing one seven-limb foreign-field shape. +const QUOTIENT_VM_TEST_LIMBS: usize = 7; +/// Gate count chosen to exceed the inline prefix plus the native-gate budget, +/// so identities are left over for the compact VM. +const QUOTIENT_VM_TEST_GATES: usize = + DEFAULT_HYBRID_QUOTIENT_INLINE_IDENTITIES + DEFAULT_QUOTIENT_NATIVE_GATES + 8; + +#[derive(Clone, Debug)] +struct QuotientVmTestConfig { + limbs: [Column; QUOTIENT_VM_TEST_LIMBS], + selector: Selector, +} + +/// Circuit whose quotient identities are numerous enough to reach the VM. +/// +/// `LoweringPlanTestCircuit` has a single gate, so its whole identity stream +/// fits in the inline prefix and the compact VM never runs. This circuit exists +/// so the fast test suite exercises the bytecode path — and therefore the +/// generator's own program certification — on a real `LoweringPlan`. +#[derive(Clone, Debug, Default)] +struct QuotientVmTestCircuit; + +impl Circuit for QuotientVmTestCircuit { + type Config = QuotientVmTestConfig; + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let limbs: [Column; QUOTIENT_VM_TEST_LIMBS] = + core::array::from_fn(|_| meta.advice_column()); + let selector = meta.selector(); + // The generator only supports one identity-committed plus one + // non-committed instance column, so mirror that shape here. + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + + meta.create_gate("quotient vm instance balance", |meta| { + let advice = meta.query_advice(limbs[0], Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + Constraints::without_selector(vec![advice + committed + public]) + }); + + // Seven-limb linear forms: the shape the LIN7 recognizer is built for. + // Distinct per-gate coefficients keep the gates from deduplicating. + for gate in 0..QUOTIENT_VM_TEST_GATES { + meta.create_gate("quotient vm limb form", move |meta| { + let terms = limbs + .iter() + .enumerate() + .map(|(limb, column)| { + let coeff = Fq::from(((gate + 1) * 16 + limb + 1) as u64); + meta.query_advice(*column, Rotation::cur()) * Expression::Constant(coeff) + }) + .reduce(|acc, term| acc + term) + .expect("limb count is nonzero"); + Constraints::without_selector(vec![terms]) + }); + } + + // One simple-selector gate so the selector fold path is covered too. + meta.create_gate("quotient vm selector form", |meta| { + let lhs = meta.query_advice(limbs[0], Rotation::cur()); + let rhs = meta.query_advice(limbs[1], Rotation::cur()); + Constraints::with_selector(selector, vec![("quotient vm selector form", lhs - rhs)]) + }); + + QuotientVmTestConfig { limbs, selector } + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), PlonkError> { + layouter.assign_region( + || "quotient vm row", + |mut region| { + config.selector.enable(&mut region, 0)?; + for column in config.limbs { + region.assign_advice(|| "limb", column, 0, || Value::known(Fq::ZERO))?; + } + Ok(()) + }, + ) + } +} + +/// Generate parameters and VK for the VM-exercising lowering-plan tests. +fn quotient_vm_test_vk() -> ( + ParamsKZG, + VerifyingKey>, +) { + let mut rng = ChaCha8Rng::seed_from_u64(11); + let params = ParamsKZG::::unsafe_setup(6, &mut rng); + let circuit = QuotientVmTestCircuit; + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 6) + .expect("quotient VM test circuit VK should build"); + (params, vk) +} + +/// H-1 (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): `NEG_S_G2_BASE` is the +/// element the deployed verifier's soundness rests on, and the tau-binding +/// pairing check in `vk.rs` is the only build-time control that ties it to +/// the commitment basis. Exercise both directions: an honest SRS passes, and +/// a G2 side taken from ANY other tau -- here the canonical generator, i.e. +/// s = 1, and a doubled s_g2, i.e. s' = 2s -- is rejected. +#[test] +fn srs_tau_binding_accepts_honest_params_and_rejects_foreign_s_g2() { + use group::{prime::PrimeCurveAffine, Curve}; + + let (params, vk) = quotient_vm_test_vk(); + let omega = vk.get_domain().get_omega(); + let honest_s_g2 = params.s_g2().to_affine(); + + assert!( + crate::lowering::vk::srs_tau_is_consistent(params.g_lagrange(), omega, honest_s_g2), + "an honestly generated SRS must pass the tau-binding pairing check" + ); + assert!( + !crate::lowering::vk::srs_tau_is_consistent( + params.g_lagrange(), + omega, + midnight_curves::G2Affine::generator(), + ), + "a G2 base substituted for s_g2 (s = 1) must fail the tau binding" + ); + assert!( + !crate::lowering::vk::srs_tau_is_consistent( + params.g_lagrange(), + omega, + (params.s_g2() + params.s_g2()).to_affine(), + ), + "a doubled s_g2 (s' = 2s) must fail the tau binding" + ); + // A mismatched domain also fails: omega from a different-sized domain + // reconstructs a different tau commitment. + assert!( + !crate::lowering::vk::srs_tau_is_consistent( + params.g_lagrange(), + omega * omega, + honest_s_g2, + ), + "a wrong domain generator must fail the tau binding" + ); +} + +/// The generator certifies its own quotient bytecode on a real plan. +/// +/// `LoweringPlan::new` runs `certify_quotient_program` and the dual-build +/// agreement check, both of which panic on mismatch, so simply building the +/// plan is the assertion. The explicit checks below guard against this test +/// silently going vacuous if the planner ever stops routing these identities +/// through the VM. +#[test] +fn lowering_plan_certifies_emitted_quotient_bytecode() { + let (params, vk) = quotient_vm_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let plan = generator.inputs().lowering_plan(); + + let interpreted = plan + .quotient + .plan + .items + .iter() + .filter(|item| matches!(item, QuotientProgramItem::Identity(_))) + .count(); + assert!( + interpreted > 0, + "test circuit no longer routes any identity through the compact VM, so the \ + certification path is untested" + ); + assert!( + !plan.quotient.build.bytes.is_empty(), + "interpreted identities should emit bytecode" + ); + assert!( + plan.quotient.build.used_ops.contains(&Q_OP_LIN7), + "test circuit should exercise the seven-limb linear recognizer; used ops: {:?}", + plan.quotient.build.used_ops + ); +} + +#[test] +fn quotient_certification_seed_binds_all_compared_artifacts() { + let build = QuotientProgramBuild { + bytes: vec![Q_OP_PUSH_CONST_U8, 0], + consts: vec![U256::from(11u64)], + max_stack: 1, + used_ops: vec![Q_OP_PUSH_CONST_U8], + used_mem_tokens: Vec::new(), + }; + let baseline = QuotientProgramBuild { + bytes: vec![Q_OP_PUSH_CONST, 0, 0], + consts: vec![U256::from(13u64)], + max_stack: 1, + used_ops: vec![Q_OP_PUSH_CONST], + used_mem_tokens: Vec::new(), + }; + let expr = QuotientExpr::Add( + Box::new(QuotientExpr::Mem(QuotientMem::Literal(0x120))), + Box::new(QuotientExpr::Const(U256::from(17u64))), + ); + let vk_payload = [0xabu8; 64]; + let seed = super::quotient_numerator::vm::certify::derive_certify_seed( + &[&build, &baseline], + &[&expr], + &vk_payload, + ); + + let mut changed_bytecode = build.clone(); + changed_bytecode.bytes[0] ^= 1; + assert_ne!( + seed, + super::quotient_numerator::vm::certify::derive_certify_seed( + &[&changed_bytecode, &baseline], + &[&expr], + &vk_payload, + ) + ); + + let mut changed_const = build.clone(); + changed_const.consts[0] = U256::from(19u64); + assert_ne!( + seed, + super::quotient_numerator::vm::certify::derive_certify_seed( + &[&changed_const, &baseline], + &[&expr], + &vk_payload, + ) + ); + + let mut changed_baseline = baseline.clone(); + changed_baseline.consts[0] = U256::from(23u64); + assert_ne!( + seed, + super::quotient_numerator::vm::certify::derive_certify_seed( + &[&build, &changed_baseline], + &[&expr], + &vk_payload, + ) + ); + + let changed_expr = QuotientExpr::Add( + Box::new(QuotientExpr::Mem(QuotientMem::Literal(0x120))), + Box::new(QuotientExpr::Const(U256::from(29u64))), + ); + assert_ne!( + seed, + super::quotient_numerator::vm::certify::derive_certify_seed( + &[&build, &baseline], + &[&changed_expr], + &vk_payload, + ) + ); + + let changed_vk_payload = [0xcdu8; 64]; + assert_ne!( + seed, + super::quotient_numerator::vm::certify::derive_certify_seed( + &[&build, &baseline], + &[&expr], + &changed_vk_payload, + ) + ); +} + /// Generate parameters and VK for lowering-plan integration tests. fn lowering_plan_test_vk() -> ( ParamsKZG, @@ -291,6 +562,121 @@ fn lowering_plan_test_vk() -> ( (params, vk) } +#[derive(Clone, Debug)] +struct RotatedPublicInstanceCircuit; + +impl Circuit for RotatedPublicInstanceCircuit { + type Config = (); + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let advice = meta.advice_column(); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + + meta.create_gate("rotated public instance", |meta| { + let advice = meta.query_advice(advice, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public_next = meta.query_instance(public_instance, Rotation::next()); + Constraints::without_selector(vec![( + "rotated public instance", + advice + committed + public_next, + )]) + }); + } + + fn synthesize( + &self, + _config: Self::Config, + _layouter: impl Layouter, + ) -> Result<(), PlonkError> { + Ok(()) + } +} + +#[test] +fn generator_rejects_rotated_non_committed_instance_queries() { + let mut rng = ChaCha8Rng::seed_from_u64(8); + let params = ParamsKZG::::unsafe_setup(4, &mut rng); + let circuit = RotatedPublicInstanceCircuit; + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 4) + .expect("test circuit VK should build"); + + assert!(matches!( + SolidityGenerator::try_new(¶ms, &vk, GeneratorConfig::new(1, 1)), + Err(GeneratorError::RotatedInstanceQuery { + column: 1, + rotation: 1, + }) + )); +} + +/// Declares two advice columns but queries only one, so the second is absorbed +/// into the transcript without ever being opened by a PCS query. +struct UnopenedAdviceColumnCircuit; + +impl Circuit for UnopenedAdviceColumnCircuit { + type Config = (); + type FloorPlanner = SimpleFloorPlanner; + type Params = (); + + fn without_witnesses(&self) -> Self { + Self + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let advice = meta.advice_column(); + // Declared and committed, but never queried: this is the shape + // `ProtocolPlan::validate` rejects. + let _unopened = meta.advice_column(); + let committed_instance = meta.instance_column(); + let public_instance = meta.instance_column(); + + meta.create_gate("unopened advice", |meta| { + let advice = meta.query_advice(advice, Rotation::cur()); + let committed = meta.query_instance(committed_instance, Rotation::cur()); + let public = meta.query_instance(public_instance, Rotation::cur()); + Constraints::without_selector(vec![("unopened advice", advice + committed + public)]) + }); + } + + fn synthesize( + &self, + _config: Self::Config, + _layouter: impl Layouter, + ) -> Result<(), PlonkError> { + Ok(()) + } +} + +/// `try_new` documents a typed error for unsupported constraint systems, so an +/// unopened advice column must not reach the `panic!` inside +/// `ProtocolPlan::from_constraint_system`. +#[test] +fn generator_reports_unopened_advice_column_as_typed_error() { + let mut rng = ChaCha8Rng::seed_from_u64(8); + let params = ParamsKZG::::unsafe_setup(4, &mut rng); + let circuit = UnopenedAdviceColumnCircuit; + let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 4) + .expect("test circuit VK should build"); + + let err = SolidityGenerator::try_new(¶ms, &vk, GeneratorConfig::new(1, 1)) + .expect_err("unopened advice column is outside the supported verifier shape"); + let GeneratorError::Planning { stage, message } = err else { + panic!("expected a planning error, got {err:?}"); + }; + assert_eq!(stage, "constraint system"); + assert!( + message.contains("absorbed but never opened"), + "error should name the unopened advice column, got {message}" + ); +} + #[test] fn scalar_le_to_be_word_reverses_exactly_one_word() { let mut le = [0u8; 32]; @@ -333,6 +719,63 @@ fn external_quotient_output_uses_planned_return_buffer() { ); } +/// The Lagrange denominator run must live entirely in the planner-registered +/// `lagrange_denoms` region. Historically it was written in place at +/// `X_N_MPTR`, overlaying theta words 27..51 and spilling into the rot_points +/// window for large instance counts -- safety then rested on write-ordering +/// coincidence. Pin the rendered source so the run cannot silently move back +/// onto the theta band. +#[test] +fn lagrange_denominator_run_uses_registered_scratch_region() { + let (params, vk) = lowering_plan_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let source = generator + .render(crate::RenderOptions::default()) + .expect("test verifier should render") + .verifier; + + assert!( + source.contains("batch_invert(success, LAGRANGE_DENOMS_MPTR"), + "Lagrange batch inversion must run over the registered denominator region" + ); + assert!( + source.contains("let mptr := LAGRANGE_DENOMS_MPTR"), + "the denominator write cursor must start at the registered region" + ); + assert!( + source.contains("mstore(X_N_MPTR, x_n)"), + "the permanent x_n theta slot must still be written by the distill step" + ); + assert!( + !source.contains("batch_invert(success, X_N_MPTR"), + "the denominator run must not be based at the X_N_MPTR theta slot" + ); + assert!( + !source.contains("add(X_N_MPTR"), + "no offset-based access to the X_N_MPTR theta slot may survive; the \ + run's offsets all belong to LAGRANGE_DENOMS_MPTR now" + ); +} + +/// The Lagrange denominator run lives in a registered scratch region that +/// grows with `num_instances`, so counts far past the historical 165-instance +/// live-memory cliff must build, validate, and render. +#[test] +fn generator_accepts_instance_counts_beyond_the_old_lagrange_cliff() { + let (params, vk) = lowering_plan_test_vk(); + let generator = SolidityGenerator::try_new(¶ms, &vk, GeneratorConfig::new(200, 1)) + .expect("large instance counts are supported with a registered denominator region"); + + let plan = generator.inputs().lowering_plan(); + plan.memory + .validate() + .expect("layout with a 200-instance denominator run is valid"); + + generator + .render(crate::RenderOptions::default()) + .expect("verifier with a 200-instance denominator run renders"); +} + #[test] fn lowering_plan_reuses_stable_layout_facts() { let (params, vk) = lowering_plan_test_vk(); @@ -381,7 +824,8 @@ fn lowering_plan_reuses_stable_layout_facts() { plan.meta.num_simple_selectors ); - let verifier = inputs.generate_verifier_from_plan(&plan, false, false, false, false, None); + let verifier = + inputs.generate_verifier_from_plan(&plan, false, false, false, false, None, None); assert_eq!(verifier.codegen_layout.proof, plan.proof_layout); assert_eq!(verifier.memory.vk_mptr, plan.vk_mptr); assert_eq!(verifier.proof_len, plan.proof_layout.proof_len); @@ -935,35 +1379,357 @@ fn transcript_memory_bound_handles_wide_bls_advice_phase() { ); } +/// A failing EIP-2537/modexp call consumes ALL forwarded gas, so every +/// precompile call site must forward the exact scheduled cost instead of +/// `gas()`: a malformed proof point then burns at most the scheduled cost of +/// the single failing call (M-2, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). +/// +/// History: this is a deliberate reversal of `2b2bf49` ("Forward gas to +/// EIP-2537 precompiles"), which removed HAND-TUNED gas literals because +/// they could brick verification on repriced chains (audit finding M-04). +/// The bounds asserted here are different in kind: they are the EIP-2537 / +/// EIP-2565 schedule formulas evaluated at generation time (layout::gas), +/// and the constructor smoke probes forward the same bounds so deployment +/// onto a repriced chain fails fast instead of bricking at proof time. #[test] -fn eip2537_calls_forward_remaining_gas() { +fn eip2537_calls_forward_exact_schedule_gas() { let verifier_template = verifier_template_corpus(); let pcs_codegen = include_str!("kzg/mod.rs"); + // Every gas() forward left in the corpus must be one of the three + // intentional sites, all in QuotientAndLinearization.yul: + // 1. the pinned external quotient evaluator staticcall (regular-call refund + // semantics: a failing callee returns unused gas; only precompile ERRORS + // burn everything forwarded), + // 2. its trace-mode call() variant, + // 3. the trace-only linearization-commitment G1MSM (never rendered into + // production artifacts). + for source in [verifier_template, pcs_codegen] { + for line in source.lines() { + if line.contains("(gas()") { + assert!( + line.contains("quotientEvaluator") || line.contains("lin_trace_ok"), + "precompile calls must forward exact EIP-2537/EIP-2565 \ + schedule gas, not gas(): {line}" + ); + } + } + } assert!( verifier_template - .contains("staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}") - && verifier_template - .contains("staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}") + .contains("staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}") && verifier_template.contains( - "staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}" - ), - "main verifier template should forward remaining gas to EIP-2537 precompiles" + "staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}" + ) + && verifier_template.contains( + "staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}" + ) + && verifier_template + .contains("staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}") + // The accumulator RHS MSM staticcall is formatted multi-line, so + // match its first argument rather than the call prefix. + && verifier_template.contains("ACC_RHS_MSM_GAS,") + && verifier_template.contains("staticcall(G1MSM_GAS_SMOKE"), + "main verifier template should forward the generated exact gas bounds" ); assert!( - pcs_codegen.contains("staticcall(gas(), 0x0c") - && pcs_codegen.contains("staticcall(gas(), 0x0b"), - "PCS emitter should forward remaining gas to EIP-2537 precompiles" + pcs_codegen.contains("staticcall(G1MSM_GAS_1PAIR, 0x0c") + && pcs_codegen.contains("staticcall(G1ADD_GAS, 0x0b"), + "PCS emitter should forward the generated exact gas bounds" ); - for source in [verifier_template, pcs_codegen] { + assert_eq!( + verifier_template.matches("(gas()").count(), + 3, + "unexpected gas() forwarding site added to the verifier templates" + ); +} + +/// Pin the generated schedule values against EIP-2537/EIP-2565 by hand so a +/// typo in the discount table or formulas cannot slip through rendering. +#[test] +fn eip2537_gas_schedule_matches_spec_vectors() { + use crate::lowering::layout::gas; + + assert_eq!(gas::G1ADD_GAS, 375); + // (k * 12000 * discount(k)) // 1000 at the table's edge cases. + assert_eq!(gas::g1msm_gas(1), 12_000); + assert_eq!(gas::g1msm_gas(2), 22_776); + assert_eq!(gas::g1msm_gas(128), 797_184); + // k > 128 keeps max_discount = 519. + assert_eq!(gas::g1msm_gas(200), 200 * 12_000 * 519 / 1_000); + // 32600*k + 37700 for the verifier's two-pair check. + assert_eq!(gas::pairing_gas(2), 102_900); + // The rendered template constants come from the same module. + let constants = crate::lowering::render::TemplateConstants::default().gas; + assert_eq!(constants.g1add, 375); + assert_eq!(constants.g1msm_one_pair, 12_000); + assert_eq!(constants.pairing_two_pair, 102_900); + assert_eq!(constants.modexp, gas::modexp_gas_word_frame()); +} + +/// MF-1: the modexp bound must cover EVERY live schedule, not just the one +/// that happened to be current when the generator was written. Derive both +/// prices here from their EIP texts instead of asserting one magic number, so +/// the next repricing forces a conscious edit rather than a silent brick: +/// `staticcall` forwards a fixed amount, so a bound below the chain's price +/// makes the precompile OOG and every proof revert. +#[test] +fn modexp_gas_bound_covers_every_live_schedule() { + use crate::lowering::layout::gas; + + // Shared inputs for the only frame the verifier emits (32-byte base, + // exponent, and modulus). + const WORDS: u64 = 32_u64.div_ceil(8); + const MULTIPLICATION_COMPLEXITY: u64 = WORDS * WORDS; + // `exponent.bit_length() - 1` for a 32-byte exponent, upper-bounded. + const ITERATION_COUNT: u64 = 255; + + // EIP-2565: `max(200, multiplication_complexity * iteration_count / 3)`. + let eip2565 = std::cmp::max(200, MULTIPLICATION_COMPLEXITY * ITERATION_COUNT / 3); + assert_eq!(eip2565, 1_360, "EIP-2565 price for the 32-byte frame"); + + // EIP-7883: the `/ 3` divisor is removed for EVERY operand size (only the + // `2 * words^2` complexity branch is width-specific) and the floor rises + // to 500: `max(500, multiplication_complexity * iteration_count)`. + let eip7883 = std::cmp::max(500, MULTIPLICATION_COMPLEXITY * ITERATION_COUNT); + assert_eq!(eip7883, 4_080, "EIP-7883 price for the 32-byte frame"); + + assert_eq!( + gas::modexp_gas_word_frame(), + std::cmp::max(eip2565, eip7883), + "modexp bound must be the maximum over live schedules" + ); + assert!( + gas::modexp_gas_word_frame() >= eip7883, + "a bound below the EIP-7883 price bricks every proof on Osaka/Fusaka \ + chains: the fixed-gas staticcall OOGs inside the mandatory Lagrange \ + batch inversion and verifyProof reverts PrecompileFailed" + ); + + // The exponent the verifier actually emits is FR_MODULUS - 2 (255 bits, + // so 254 iterations); the generic bound must cover its exact price too. + assert!(gas::modexp_gas_word_frame() >= MULTIPLICATION_COMPLEXITY * 254); +} + +/// MF-4: the typed taxonomy exists so an incident responder can tell a chain +/// fault from a rejected proof. Three paths used to conflate them -- a failed +/// precompile inside the accumulator precheck or the final pairing surfaced as +/// an input rejection, and a zero Lagrange denominator (a transcript event) +/// surfaced as `PrecompileFailed`. Pin the split so it cannot regress. +#[test] +fn precompile_faults_and_input_rejections_use_distinct_selectors() { + let corpus = verifier_template_corpus(); + + // (a) The pairing helper: staticcall/returndatasize failure is a chain + // fault; a pairing that ran and returned != 1 rejects the proof. + assert!( + corpus.contains( + "if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) }\n // Compare against 1 rather than truncating to the low bit:" + ), + "ec_pairing must report a failed pairing staticcall as PrecompileFailed" + ); + assert!( + corpus.contains("ret := eq(mload(scratch), 1)\n if iszero(ret) { fail(ERR_PROOF_REJECTED) }"), + "ec_pairing must report a pairing result of 0 as ProofRejected" + ); + + // (b) batch_invert and the accumulator validator both carry a cause flag + // rather than a bare boolean. + assert!( + corpus.contains( + "function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret, precompile_failed" + ), + "batch_invert must report whether its modexp call failed" + ); + assert!( + corpus + .contains("function validate_public_accumulator(success, r) -> out, precompile_failed"), + "the accumulator validator must report whether its G1MSM call failed" + ); + + // (c) Both boundaries must branch on that flag. + for (guarded, fallback, what) in [ + ( + "if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }", + "fail(ERR_PROOF_REJECTED)", + "Lagrange", + ), + ( + "if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }", + "fail(ERR_BAD_POINT_ENCODING)", + "accumulator", + ), + ] { + assert!( + corpus.contains(guarded) && corpus.contains(fallback), + "{what} boundary must split precompile faults from rejected input" + ); + } + + // A zero denominator means the squeezed x hit a domain point: an input + // rejection, not a broken chain. + assert!( + !corpus.contains( + "success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r)" + ), + "the Lagrange batch inversion must thread its failure cause" + ); +} + +/// MF-3: the quotient VM's terminal checks (`q_pc == q_end`, `q_has_top == 0`, +/// `q_sp == base`) cannot see three failure shapes, so the interpreter grew +/// guards for each. The program is VK-codehash-pinned, so none of these are +/// reachable on-chain with a well-formed artifact -- they are containment for +/// a future generator bug, mirroring on the deployed side what the reference +/// VM already enforces at build time (`stack.len() == 1` per identity, +/// `const_at` bounds, and `identity_segment` rejecting a native marker inside +/// an expression). +#[test] +fn quotient_vm_interpreter_fails_closed_on_malformed_programs() { + let vm = include_str!("../../templates/partials/quotient_numerator/QuotientNumeratorBlock.yul"); + + // (a) A fold with no live cached top would re-fold a STALE q_top, and + // both terminal checks would still pass. + assert_eq!( + vm.matches("{%- call q_top_guard() %}").count(), + 2, + "both FOLD_MAIN and FOLD_SELECTOR must require a live cached top" + ); + assert!( + vm.contains("if iszero(q_has_top) { q_program_fail() }"), + "q_top_guard must fail closed when the cached top is not live" + ); + + // (b) Native callbacks used to RESET q_sp, which silently discarded + // operands spilled by a preceding partial expression -- an identity would + // drop out of nu_y(x) with the program still ending balanced. Assert + // instead of reset. + // The only assignment of the stack base to q_sp may be its declaration. + assert_eq!( + vm.matches("q_sp := {{ program.stack_mptr|hex() }}").count(), + 1, + "native callbacks must not reset q_sp; a reset hides dropped operands" + ); + assert!( + vm.contains("let q_sp := {{ program.stack_mptr|hex() }}"), + "the surviving q_sp assignment must be its initial declaration" + ); + assert_eq!( + vm.matches("{%- call q_stack_empty_guard() %}").count(), + 3, + "each native callback boundary must assert an already-empty stack" + ); + + // (c) The spill pointer has no ceiling of its own. + assert_eq!( + vm.matches("if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() }") + .count(), + 10, + "every cached-top spill site must clamp q_sp to its registered region" + ); + + // (d) u16 constant-table indexes reach far outside the pinned payload, so + // unlike the u8 forms they cannot rely on bounded drift. + assert_eq!( + vm.matches("{%- call q_const_guard(\"qconst\") %}").count(), + 3, + "u16 constant-table indexes must be clamped to the rendered table length" + ); + assert!( + vm.contains("if iszero(lt({{ idx }}, {{ program.num_consts }})) { q_program_fail() }"), + "q_const_guard must clamp against the generated constant-table length" + ); +} + +/// MF-2: the free-memory-pointer guard is the only on-chain check that solc's +/// stack-spill reservation has not grown into the generated absolute layout. +/// A fork that recompiles at a different (version, optimiser-runs) pair can +/// still fit EIP-170, deploy, and then revert on every proof -- so the guard +/// must say *why* rather than reverting bare, which is indistinguishable from +/// every other empty revert. +#[test] +fn memory_layout_guard_reverts_with_a_typed_selector() { + let verifier_template = include_str!("../../templates/contracts/Halo2Verifier.sol"); + let smoke_template = include_str!("../../templates/partials/verifier/PrecompileSmoke.sol"); + + for (source, name) in [ + (verifier_template, "verifyProof"), + (smoke_template, "constructor"), + ] { + assert!( + source.contains("mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED))") + && source.contains("revert(0x00, 0x04)"), + "{name} memory-layout guard must revert with MemoryLayoutViolated()" + ); + } + // The guard must still be the first thing each assembly block does: it + // protects the writes that follow, so a bare `revert(0, 0)` left behind + // on either path means the typed rewrite missed a site. + assert!( + verifier_template.contains("if gt(mload(0x40), TRANSCRIPT_MPTR) {"), + "runtime guard must still compare the FMP against TRANSCRIPT_MPTR" + ); + assert!( + !verifier_template.contains("if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) }"), + "runtime memory-layout guard still reverts bare" + ); + assert!( + !smoke_template.contains( + "if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { revert(0, 0) }" + ), + "constructor memory-layout guard still reverts bare" + ); +} + +/// MF-1: modexp is the one precompile the runtime cannot do without -- the +/// Lagrange batch inversion calls it on every proof -- and it was the one +/// precompile the constructor never probed, so a stale bound deployed +/// silently. Pin the probe's shape: right precompile, the pinned runtime +/// bound (not `gas()`), a return-size check, and a known answer a stub +/// cannot satisfy. +#[test] +fn constructor_probes_modexp_at_the_pinned_runtime_bound() { + let smoke = include_str!("../../templates/partials/verifier/PrecompileSmoke.sol"); + + assert!( + smoke.contains("staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}"), + "constructor must probe modexp at the same bound the runtime forwards" + ); + assert!( + smoke.contains( + "if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) }" + ), + "modexp probe must check the returned size" + ); + // 2^(r-2) == 2^-1, verified as mulmod(result, 2, r) == 1: a precompile + // that returns zeros, or echoes its input, fails this. + assert!( + smoke.contains("mstore(add(scratch, {{ template_constants.modexp.base_offset|hex() }}), 2)") + && smoke.contains( + "mstore(add(scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2))" + ) + && smoke.contains("if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) }"), + "modexp probe must run the runtime's own Fermat inversion as a known-answer test" + ); + + // Every precompile the runtime calls is now probed at its pinned bound. + for (probe, gas_constant) in [ + ("modexp.address", "MODEXP_GAS"), + ("eip2537.g1add_address", "G1ADD_GAS"), + ("eip2537.g1msm_address", "G1MSM_GAS_1PAIR"), + ("eip2537.pairing_address", "PAIRING_GAS_2PAIR"), + ] { assert!( - !source.contains("g1msm_gas_cap") - && !source.contains("G1ADD_GAS_CAP") - && !source.contains("PAIRING_SMOKE_GAS_CAP") - && !source.contains("final_pairing_gas_cap"), - "EIP-2537 gas-cap literals must not be rendered or computed" + smoke.contains(&format!( + "staticcall({gas_constant}, {{{{ template_constants.{probe}|hex() }}}}" + )), + "constructor smoke probe missing for {probe} at {gas_constant}" ); } + assert!( + !smoke.contains("staticcall(gas()"), + "smoke probes must forward pinned bounds, not gas()" + ); } #[test] @@ -972,22 +1738,22 @@ fn failed_success_paths_do_not_enter_ec_precompiles() { let pcs_codegen = include_str!("kzg/mod.rs"); assert!( - verifier_template.contains("if iszero(success) { revert(0, 0) }\n }\n\n {%- if self.expected_has_accumulator %}\n // Fail malformed accumulator public inputs before transcript"), + verifier_template.contains("if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) }\n }\n\n {%- if self.expected_has_accumulator %}\n // Fail malformed accumulator public inputs before transcript"), "ABI/proof length/instance shape checks should fail before accumulator or transcript parsing" ); assert!( - verifier_template.contains("success := validate_public_accumulator(success, r)\n if iszero(success) { revert(0, 0) }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), + verifier_template.contains("success, acc_precompile_failed := validate_public_accumulator(success, r)\n if iszero(success) {\n // MF-4: a G1MSM that could not run at all is a chain fault,\n // not a malformed accumulator point.\n if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }\n fail(ERR_BAD_POINT_ENCODING)\n }\n {%- endif %}\n\n {%- if self.gas_checkpoints %}\n gas_checkpoint(2)"), "accumulator precheck should fail before transcript parsing" ); assert!( verifier_template.contains( - "success := and(success, lt(inst_be, r))\n // Instances are passed BE in calldata, matching the\n // Keccak Fq transcript input.\n buf_len := common_word(buf_len, inst_be)\n }\n if iszero(success) { revert(0, 0) }" + "success := and(success, lt(inst_be, r))\n // Instances are passed BE in calldata, matching the\n // Keccak Fq transcript input.\n buf_len := common_word(buf_len, inst_be)\n }\n if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) }" ), "non-canonical public instances should fail before proof transcript parsing" ); assert!( verifier_template.contains( - "if iszero(success) { revert(0, 0) }\n\n {%- match quotient_external %}" + "if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) }\n fail(ERR_PROOF_REJECTED)\n }\n\n {%- match quotient_external %}" ), "failed Lagrange/common-polynomial setup should fail before quotient reconstruction" ); @@ -1003,18 +1769,51 @@ fn failed_success_paths_do_not_enter_ec_precompiles() { } assert!( verifier_template.contains( - "if iszero(success) { revert(0, 0) }\n success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR)" + "if iszero(success) { fail(ERR_PRECOMPILE_FAILED) }\n success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR)" ), "final pairing block should revert before staging/calling the pairing precompile when success is already false" ); assert!( pcs_codegen.contains("if success {") - && pcs_codegen.contains("success := staticcall(gas(), 0x0c") - && pcs_codegen.contains("success := staticcall(gas(), 0x0b"), + && pcs_codegen.contains("success := staticcall(G1MSM_GAS_1PAIR, 0x0c") + && pcs_codegen.contains("success := staticcall(G1ADD_GAS, 0x0b"), "PCS emitter should guard final MSM/add precompile calls with if success" ); } +/// The constructor's known-answer probe is only as good as its constants: a +/// wrong 2G would brick every deployment, and a 2G that happened to equal the +/// probe's own input would silently restore the identity-only weakness. Pin +/// both against the curve library rather than against hardcoded literals. +#[test] +fn constructor_known_answer_vector_is_the_generator_and_its_double() { + use group::{prime::PrimeCurveAffine, Curve, Group}; + use midnight_curves::{G1Affine, G1Projective}; + + let constants = crate::lowering::render::TemplateConstants::default().eip2537; + let expected_g = crate::lowering::encoding::g1_to_u256s(G1Affine::generator()); + let g = G1Projective::generator(); + let expected_2g = crate::lowering::encoding::g1_to_u256s((g + g).to_affine()); + + assert_eq!( + constants.g1_generator, + (expected_g[0], expected_g[1], expected_g[2], expected_g[3]) + ); + assert_eq!( + constants.g1_double_generator, + ( + expected_2g[0], + expected_2g[1], + expected_2g[2], + expected_2g[3] + ) + ); + assert_ne!( + constants.g1_generator, constants.g1_double_generator, + "a known-answer probe whose expected output equals its input tests nothing" + ); +} + #[test] fn verifier_constructor_smoke_tests_runtime_prerequisites() { let verifier_template = verifier_template_corpus(); @@ -1024,17 +1823,25 @@ fn verifier_constructor_smoke_tests_runtime_prerequisites() { "generated verifier should include a deployment-time runtime prerequisite smoke test" ); for required in [ - "Smoke-check the Cancun/EIP-2537 runtime features", + "Smoke-check the Cancun/EIP-2537/modexp runtime features", "mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }})", "eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)", "non-Cancun fork fails during deployment", + // MF-1: modexp is a runtime prerequisite like any other precompile. + "modexp (0x05) known-answer probe at the pinned runtime bound", + "staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}", + "if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) }", "G1ADD(identity, identity) -> identity", + "Known-answer probe: G1ADD(G, G) == 2G", + "template_constants.eip2537.g1_generator", + "template_constants.eip2537.g1_double_generator", "Worst-case generated G1MSM with all identity/zero terms", "constructor_g1msm_smoke_input_bytes", "PAIRING_CHECK([(identity_g1, identity_g2), (identity_g1, identity_g2)])", - "staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}", - "staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}", - "staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}", + "staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}", + "staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}", + "staticcall(G1MSM_GAS_SMOKE, {{ template_constants.eip2537.g1msm_address|hex() }}", + "staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}", "template_constants.eip2537.g1add_address", "template_constants.eip2537.g1msm_address", "template_constants.eip2537.pairing_address", @@ -1206,9 +2013,9 @@ fn accumulator_points_are_prevalidated_before_transcript_work() { let verifier_template = verifier_template_corpus(); for required in [ - "function validate_public_accumulator(success, r) -> out", + "function validate_public_accumulator(success, r) -> out, precompile_failed", "Fail malformed accumulator public inputs before transcript", - "success := validate_public_accumulator(success, r)", + "success, acc_precompile_failed := validate_public_accumulator(success, r)", "gas_checkpoint(2) // after VK loading + accumulator public-input precheck", "Batch the prevalidated public IVC accumulator pairing equation", ] { @@ -1219,6 +2026,26 @@ fn accumulator_points_are_prevalidated_before_transcript_work() { } } +#[test] +fn accumulator_scalars_are_range_checked_where_they_are_read() { + let verifier_template = verifier_template_corpus(); + + // `validate_public_accumulator` runs before the transcript instance loop + // that rejects non-canonical instance words, and EIP-2537 G1MSM reduces + // scalars mod r implicitly. Canonicality must therefore be enforced at the + // read sites in this helper rather than inherited from a later template. + for required in [ + "out := and(out, lt(lhs_scalar, r))", + "out := and(out, lt(rhs_scalar, r))", + "out := and(out, lt(fixed_scalar_{{ loop.index0 }}, r))", + ] { + assert!( + verifier_template.contains(required), + "accumulator scalars must be range-checked against r where they are read: {required}" + ); + } +} + #[test] fn accumulator_decoder_rejects_noncanonical_infinity() { let verifier_template = verifier_template_corpus(); @@ -1296,9 +2123,13 @@ fn generated_solidity_pragmas_require_mcopy_capable_compiler() { include_str!("../../templates/contracts/Halo2QuotientEvaluator.sol"), ), ] { + // Pinned (not ^-ranged) since the M-1 fix: the artifact hashes in the + // review packet are only reproducible against one compiler version, + // and 0.8.30 is the version the deployment pipeline pins. assert!( - source.contains("pragma solidity ^0.8.24;"), - "{name} must require Solidity 0.8.24+ for Cancun Yul opcodes" + source.contains("pragma solidity 0.8.30;"), + "{name} must pin the Solidity version the build pipeline pins \ + (0.8.30, MCOPY/Cancun-capable)" ); } } @@ -1432,7 +2263,10 @@ fn differential_trace_hooks_cover_expected_categories() { "serialized PCS point sets", "trace::PCS_SERIALIZED_POINT_SET_BASE + set_idx as u64", ), - ("PCS q_com commitments", "40000 + set_idx"), + ( + "PCS q_com commitments", + "trace::PCS_Q_COM_BASE + set_idx as u64", + ), ] { assert!( pcs_source.contains(needle), @@ -1560,8 +2394,8 @@ fn quotient_vm_runtime_asserts_exact_program_termination() { let verifier_template = verifier_template_corpus(); assert!( - verifier_template.contains("if iszero(eq(q_pc, q_end)) { revert(0, 0) }") - && verifier_template.contains("if q_has_top { revert(0, 0) }"), + verifier_template.contains("if iszero(eq(q_pc, q_end)) { q_program_fail() }") + && verifier_template.contains("if q_has_top { q_program_fail() }"), "quotient VM must fail closed when bytecode over-runs q_end or leaves a live stack value" ); } @@ -1689,6 +2523,26 @@ fn batch_invert_handles_empty_and_singleton_ranges() { verifier_template.contains("if ret { mstore(mptr_start, mload(single_scratch)) }"), "singleton batch inversion must store the single inverse in place" ); + // The general path must reject non-canonical words (x >= r) like the + // singleton path, so accept/reject semantics do not depend on batch + // length: one guard on the first element, one inside the prefix-product + // loop, one on the final element. + assert_eq!( + verifier_template.matches("if iszero(lt(gp, r)) {").count(), + 1, + "general batch inversion path must range-check the first element" + ); + assert_eq!( + verifier_template.matches("if iszero(lt(x, r)) {").count(), + 2, + "batch inversion must range-check the singleton element and every \ + prefix-product loop element" + ); + assert_eq!( + verifier_template.matches("if iszero(lt(x_last, r)) {").count(), + 1, + "general batch inversion path must range-check the final element" + ); } #[test] @@ -1715,8 +2569,14 @@ fn production_verifier_documents_revert_or_true_policy() { verifier_template.contains("function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret") && verifier_template .contains("ret := success\n if iszero(ret) { leave }") + // MF-4: a failed/short-returning pairing staticcall is a chain + // fault (PrecompileFailed); only a pairing that RAN and + // returned != 1 rejects the proof. + && verifier_template.contains( + "ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }}))\n if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) }", + ) && verifier_template.contains( - "ret := and(ret, mload(scratch))\n if iszero(ret) { revert(0, 0) }\n ret := 1", + "ret := eq(mload(scratch), 1)\n if iszero(ret) { fail(ERR_PROOF_REJECTED) }\n ret := 1", ), "final pairing helper must revert on pairing failure and normalize success to one" ); @@ -1735,6 +2595,112 @@ fn production_verifier_documents_revert_or_true_policy() { ); } +/// P10 (L-8): the emitted BUILD_ID must be a stable function of the build's +/// identity components, and the optional deployment provenance tag must be +/// the ONLY thing that changes when it is supplied. +#[test] +fn build_id_identifies_the_build_and_its_provenance() { + let (params, vk) = quotient_vm_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let extract = |source: &str| { + source + .lines() + .find(|line| line.contains("BUILD_ID = 0x")) + .expect("rendered verifier carries a BUILD_ID constant") + .trim() + .to_string() + }; + + let base = generator + .render(RenderOptions::default()) + .expect("default render succeeds") + .verifier; + let again = generator + .render(RenderOptions::default()) + .expect("repeat render succeeds") + .verifier; + assert_eq!( + base, again, + "renders with equal options must be byte-identical" + ); + + let tagged = generator + .render(RenderOptions { + provenance: Some([0x42; 32]), + ..RenderOptions::default() + }) + .expect("provenance render succeeds") + .verifier; + assert_ne!( + extract(&base), + extract(&tagged), + "the provenance tag must change BUILD_ID" + ); + assert_eq!( + base.replace(&extract(&base), ""), + tagged.replace(&extract(&tagged), ""), + "the provenance tag must change ONLY the BUILD_ID constant" + ); +} + +/// P4 (L-3): the Yul revert sites carry hardcoded 4-byte selectors while the +/// Solidity ABI carries the `error` declarations; pin the two against each +/// other so neither can drift silently. +#[test] +fn p4_error_selectors_match_declared_errors() { + use sha3::{Digest, Keccak256}; + + let verifier_template = include_str!("../../templates/contracts/Halo2Verifier.sol"); + let constants_template = include_str!("../../templates/partials/verifier/Constants.sol"); + let helpers_template = + include_str!("../../templates/partials/quotient_numerator/QuotientHelpers.yul"); + + for (signature, constant_name) in [ + ("BadCalldataShape()", "ERR_BAD_CALLDATA_SHAPE"), + ("VkMismatch()", "ERR_VK_MISMATCH"), + ("NonCanonicalScalar()", "ERR_NON_CANONICAL_SCALAR"), + ("BadPointEncoding()", "ERR_BAD_POINT_ENCODING"), + ("PrecompileFailed()", "ERR_PRECOMPILE_FAILED"), + ("ProofRejected()", "ERR_PROOF_REJECTED"), + ("QuotientProgramInvalid()", "ERR_QUOTIENT_PROGRAM_INVALID"), + // MF-2: the memory-layout guard reports a build fault (a recompile + // whose spill region reaches the generated layout), so it must be + // decodable rather than an anonymous empty revert. + ("MemoryLayoutViolated()", "ERR_MEMORY_LAYOUT_VIOLATED"), + ] { + let digest = Keccak256::digest(signature.as_bytes()); + let selector = format!( + "0x{:02x}{:02x}{:02x}{:02x}", + digest[0], digest[1], digest[2], digest[3] + ); + let error_name = signature.trim_end_matches("()"); + assert!( + verifier_template.contains(&format!("error {error_name}();")), + "Halo2Verifier.sol must declare `error {error_name}();` so the ABI carries it" + ); + assert!( + constants_template.contains(&selector), + "Constants.sol selector for {signature} must be {selector}" + ); + assert!( + constants_template.contains(constant_name), + "Constants.sol must define {constant_name}" + ); + } + // The quotient VM's dedicated helper hardcodes the QuotientProgramInvalid + // selector because it renders in both the verifier and the standalone + // evaluator assembly. + let digest = Keccak256::digest(b"QuotientProgramInvalid()"); + let selector = format!( + "0x{:02x}{:02x}{:02x}{:02x}", + digest[0], digest[1], digest[2], digest[3] + ); + assert!( + helpers_template.contains(&format!("shl(224, {selector})")), + "q_program_fail must hardcode the QuotientProgramInvalid selector {selector}" + ); +} + #[test] fn templates_do_not_write_solidity_reserved_memory_slots() { let verifier_template = verifier_template_corpus(); @@ -1750,6 +2716,13 @@ fn templates_do_not_write_solidity_reserved_memory_slots() { ("QuotientHelpers.yul", quotient_helpers), ("Halo2VerifyingKey.sol", vk_template), ] { + // The one sanctioned use of Solidity's 0x00 scratch word: the typed + // custom-error revert idiom (P4/L-3) writes a 4-byte selector there + // immediately before reverting. It is terminal and 0x00..0x3f is + // legal scratch, so exempt exactly that pattern and keep every other + // low-memory write forbidden. + let source = source.replace("mstore(0x00, shl(224, ", ""); + let source = source.as_str(); for needle in [ "mstore(0,", "mstore(0x00,", @@ -2388,6 +3361,49 @@ fn quotient_vm_bilin7_pairwise_matches_direct_expr_eval() { ); } +#[test] +fn quotient_vm_limb_decomposition_survives_const_table_overflow() { + // A large affine sum over consecutive limb pointers is emitted as a chain of + // LIN7 opcodes whose coefficients land in the one-byte constant table. With + // more than 256 distinct coefficients the table overflows a `u8` slot + // partway through emission. Before the fix, `emit_limb_shape`'s + // `u8::try_from(slot).expect(...)` panicked once the shape being emitted was + // preceded by enough residue constants; the decomposition path now re-checks + // the post-residue table and falls back to generic ops for the overflowing + // shape. This exercises that fallback and confirms it still evaluates the + // expression correctly. + let term_count = 300u32; + let mut values = HashMap::new(); + let mut expr = QuotientExpr::Const(U256::ZERO); + for k in 0..term_count { + let ptr = 0x1000 + k * 0x20; + values.insert(ptr, Fq::from(17 + k as u64)); + expr = quotient_add_expr( + expr, + // Coefficient `k + 2` keeps every term scaled (coeff 1 would drop the + // constant) and distinct, so the table grows one slot per term. + quotient_scale_expr( + Fq::from(k as u64 + 2), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + // The pre-fix builder panics inside this call for this input. + builder.emit_expr(&expr); + + assert!( + builder.consts.len() > u8::MAX as usize, + "test must overflow the one-byte constant table to exercise the fallback" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + #[test] fn quotient_vm_pow5_matches_direct_expr_eval() { let ptr = 0xa20; @@ -2439,6 +3455,41 @@ fn quotient_vm_pow5_rejects_near_miss_product_shapes() { ); } +#[test] +fn quotient_vm_add_product_reserves_scalar_before_base_constants() { + let lhs = 0xaa0; + let rhs = 0xac0; + let mut values = HashMap::new(); + values.insert(lhs, Fq::from(149u64)); + values.insert(rhs, Fq::from(157u64)); + + let mut base = QuotientExpr::Const(U256::ZERO); + for value in 1..=255u64 { + base = quotient_add_expr(base, QuotientExpr::Const(U256::from(value))); + } + let product = quotient_mul_expr( + quotient_mul_expr( + QuotientExpr::Mem(QuotientMem::Literal(lhs)), + QuotientExpr::Mem(QuotientMem::Literal(rhs)), + ), + QuotientExpr::Const(U256::from(300u64)), + ); + let expr = quotient_add_expr(base, product); + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::default(); + builder.emit_expr(&expr); + + assert!( + builder.bytes.contains(&Q_OP_ADD_MUL_MEM_MEM_CONST_U8), + "product should stay on the fused add-mul path" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + #[test] fn quotient_vm_limb_subshape_matches_direct_expr_eval() { let mut values = HashMap::new(); @@ -2486,6 +3537,53 @@ fn quotient_vm_limb_subshape_matches_direct_expr_eval() { ); } +#[test] +fn quotient_vm_limb_decomposition_reserves_shape_coeffs_before_residue() { + let mut values = HashMap::new(); + let mut expr = QuotientExpr::Const(U256::ZERO); + + for i in 0..7u32 { + let ptr = 0xb00 + i * WORD_BYTES as u32; + values.insert(ptr, Fq::from(151 + i as u64)); + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(19 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + for i in 0..256u32 { + let ptr = 0x2000 + i * 0x40; + values.insert(ptr, Fq::from(401 + i as u64)); + expr = quotient_add_expr( + expr, + quotient_scale_expr( + Fq::from(1000 + i as u64), + QuotientExpr::Mem(QuotientMem::Literal(ptr)), + ), + ); + } + + let expected = eval_quotient_expr_for_test(&expr, &values); + let mut builder = QuotientProgramBuilder::with_limb_vm_ops(true); + builder.emit_expr(&expr); + + assert!( + builder.consts.len() > u8::MAX as usize, + "residue should grow the constant table past u8" + ); + assert!( + builder.bytes.contains(&Q_OP_LIN7), + "larger affine sums should still extract LIN7 subshapes" + ); + assert_eq!( + eval_quotient_vm_for_test(&builder.bytes, &builder.consts, &values), + expected + ); +} + #[test] fn quotient_vm_limb_subshape_inside_conditional_product_matches_direct_expr_eval() { let mut values = HashMap::new(); @@ -3290,3 +4388,107 @@ fn fq_from_u256(value: U256) -> Fq { let repr = ::Repr::from(bytes); Option::::from(Fq::from_repr(repr)).expect("canonical field element") } + +/// The pointer walker must know every opcode's memory operands. +/// +/// `quotient_read_pointers` fails closed on an unhandled opcode rather than +/// skipping it, so this test is what makes the fallback arm reachable +/// information: a new opcode added to `QUOTIENT_OPCODE_TABLE` without extending +/// the walker fails here instead of silently shipping unchecked pointers. +#[test] +fn quotient_pointer_walker_covers_every_opcode() { + let model = QuotientReadModel::default(); + for spec in QUOTIENT_VM_SPEC.opcodes { + // Zero operands keep every embedded count at zero, so the walk + // terminates for the dynamic opcodes without needing a hand-built + // program per shape. + let mut bytes = vec![spec.opcode]; + bytes.extend(std::iter::repeat_n(0u8, 64)); + let result = quotient_read_pointers(&bytes, 0, spec.opcode, &model, &mut |_, _| Ok(())); + if let Err(err) = result { + assert!( + !err.contains("pointer walker does not handle"), + "opcode {} ({:#x}) has no arm in quotient_read_pointers: {err}", + spec.name, + spec.opcode + ); + } + } +} + +/// Every pointer a real plan emits lands in a window the verifier populates. +/// +/// The positive assertion alone could pass on a program with no memory +/// operands at all, so this also pins that the fixture actually exercises the +/// walker. +#[test] +fn quotient_program_pointers_stay_inside_populated_windows() { + let (params, vk) = quotient_vm_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let plan = generator.inputs().lowering_plan(); + let model = plan.quotient_read_model(); + + let mut pointers = Vec::new(); + for (idx, op, _len) in quotient_bytecode_ops(&plan.quotient.build.bytes) { + quotient_read_pointers( + &plan.quotient.build.bytes, + idx, + op, + &model, + &mut |_kind, ptr| { + pointers.push(ptr); + Ok(()) + }, + ) + .expect("pointer walk should decode a validated program"); + } + assert!( + !pointers.is_empty(), + "test circuit emits no VM memory operands, so the pointer validator is vacuous here" + ); + + // `LoweringPlan::new` already ran this; assert directly so the failure + // message points at this invariant rather than at plan construction. + validate_quotient_mem_ptrs(&plan.quotient.build.bytes, &model) + .expect("emitted pointers should all land in populated windows"); +} + +/// A pointer outside the populated windows is rejected. +/// +/// Walks the real program, finds the first `LIN7` limb pointer, and rewrites it +/// to an address inside the verifier's low-memory scratch -- readable at +/// runtime, but holding transcript/pairing state rather than the proof +/// evaluation the identity expects. This is the shape a `Data` or planner +/// regression would take, and it is exactly what certification cannot see. +#[test] +fn quotient_pointer_validator_rejects_out_of_window_reads() { + let (params, vk) = quotient_vm_test_vk(); + let generator = SolidityGenerator::new(¶ms, &vk, GeneratorConfig::new(1, 1)); + let plan = generator.inputs().lowering_plan(); + let model = plan.quotient_read_model(); + + let mut corrupted = plan.quotient.build.bytes.clone(); + let limb_ptr_offset = quotient_bytecode_ops(&plan.quotient.build.bytes) + .find_map(|(idx, op, _)| (op == Q_OP_LIN7).then_some(idx + 2)) + .expect("test circuit should emit a seven-limb linear form"); + let stray = u16::try_from(layout::LOW_MEMORY_SCRATCH_START).expect("scratch base fits u16"); + corrupted[limb_ptr_offset..limb_ptr_offset + 2].copy_from_slice(&stray.to_be_bytes()); + assert_ne!( + corrupted, plan.quotient.build.bytes, + "corruption should change the program" + ); + + let err = validate_quotient_mem_ptrs(&corrupted, &model) + .expect_err("a pointer into low-memory scratch must be rejected"); + assert!( + err.contains("outside every window"), + "unexpected rejection reason: {err}" + ); + + // The stack-safety and const-slot validators pass on the same bytes, so + // this really is coverage they do not provide. + validate_quotient_program(&corrupted) + .expect("corrupting a pointer must not change structural validity"); + validate_quotient_const_slots(&corrupted, plan.quotient.build.consts.len()) + .expect("corrupting a pointer must not change const-slot validity"); +} diff --git a/proofs/solidity-verifier/src/lowering/vk.rs b/proofs/solidity-verifier/src/lowering/vk.rs index 480b68c37..198065171 100644 --- a/proofs/solidity-verifier/src/lowering/vk.rs +++ b/proofs/solidity-verifier/src/lowering/vk.rs @@ -6,9 +6,9 @@ //! generated verifier contracts. use ff::Field; -use group::{prime::PrimeCurveAffine, Curve}; +use group::{prime::PrimeCurveAffine, Curve, Group}; use itertools::chain; -use midnight_curves::{Fq, G1Affine, G1Projective, G2Affine}; +use midnight_curves::{pairing::Engine, Bls12, Fq, G1Affine, G1Projective, G2Affine}; use ruint::aliases::U256; use crate::lowering::{ @@ -26,7 +26,71 @@ use crate::lowering::{ VerifierBuildInputs, }; +/// Return whether `s_g2` corresponds to the same tau that produced +/// `g_lagrange`. +/// +/// Commit f(X) = X in the Lagrange basis over the domain generated by +/// `omega` to obtain `[tau]G1 = sum_i omega^i * L_i(tau) * G`, then check +/// the pairing identity `e([tau]G1, G2) == e(G1, s_g2)`. This ties the G2 +/// side of the key -- the element the deployed verifier's final pairing +/// trusts as `[s]_2` -- to the commitment basis the prover actually uses. +pub(crate) fn srs_tau_is_consistent( + g_lagrange: &[G1Projective], + omega: Fq, + s_g2: G2Affine, +) -> bool { + let tau_g1 = srs_tau_commitment(g_lagrange, omega); + Bls12::pairing(&tau_g1, &G2Affine::generator()) == Bls12::pairing(&G1Affine::generator(), &s_g2) +} + +/// Commit f(X) = X in the Lagrange basis: `[tau]G1 = sum_i omega^i * L_i`. +/// +/// Used by the tau-binding pairing check above and as the tau component of +/// the SRS fingerprint folded into `BUILD_ID` (P10/L-8). +pub(crate) fn srs_tau_commitment(g_lagrange: &[G1Projective], omega: Fq) -> G1Affine { + // Pippenger MSM: production SRS sizes reach 2^20 Lagrange points, so a + // naive per-point scalar multiplication here would stall every build. + let mut omega_pow = Fq::ONE; + let omega_powers: Vec = (0..g_lagrange.len()) + .map(|_| { + let current = omega_pow; + omega_pow *= omega; + current + }) + .collect(); + G1Projective::multi_exp(g_lagrange, &omega_powers).to_affine() +} + impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { + /// A 32-byte identity for the SRS this build trusts, for `BUILD_ID` + /// (P10/L-8, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md). + /// + /// An SRS over a fixed curve is fully determined by its size `n` and the + /// secret tau, so hash exactly the values that pin those: `n`, the G2 + /// base, `s_g2 = [tau]G2`, and `[tau]G1` recomputed from the Lagrange + /// basis actually used for commitments. Two params values agree on this + /// fingerprint iff they present the same trusted setup to the verifier. + pub(crate) fn srs_fingerprint(&self) -> [u8; 32] { + use sha3::{Digest, Keccak256}; + + let g_lagrange = self.params.g_lagrange(); + let omega = self.vk.get_domain().get_omega(); + let tau_g1 = srs_tau_commitment(g_lagrange, omega); + let mut hasher = Keccak256::new(); + hasher.update(b"halo2-solidity-verifier-srs-v1"); + hasher.update((g_lagrange.len() as u64).to_be_bytes()); + for word in g2_to_u256s(self.params.g2().to_affine()) { + hasher.update(word.to_be_bytes::<32>()); + } + for word in g2_to_u256s(self.params.s_g2().to_affine()) { + hasher.update(word.to_be_bytes::<32>()); + } + for word in g1_to_u256s(tau_g1) { + hasher.update(word.to_be_bytes::<32>()); + } + hasher.finalize().into() + } + /// Generate the VK payload before compact quotient constants/program data. fn generate_base_vk(&self) -> Halo2VerifyingKey { let constants: Vec<(&'static str, U256)>; @@ -69,9 +133,76 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { // (4 / 8 u256 words respectively). We cannot read `params.g[0]` // directly (the field is crate-private in midnight-proofs), so // we use the canonical BLS12-381 generator. + // + // The generated verifier subtracts `v * G1_BASE` in the final KZG + // linearization while every commitment in the proof/VK is over the + // SRS base `g[0]`. If a deployer-supplied `params` had `g[0] != G`, + // the contract would enforce a different pairing equation than the + // native verifier (bricking honest proofs, or accepting evaluation + // claims for a scaled statement). Validate consistency at build + // time: the commitment of the constant-1 polynomial equals `g[0]`, + // and in the Lagrange basis that commitment is `sum(g_lagrange)`. + let g_lagrange = self.params.g_lagrange(); + let srs_g1_base = g_lagrange + .iter() + .copied() + .fold(G1Projective::identity(), |acc, g| acc + g) + .to_affine(); + assert_eq!( + srs_g1_base, + G1Affine::generator(), + "SRS G1 base (sum of g_lagrange) is not the canonical BLS12-381 \ + generator; the emitted G1_BASE would diverge from the commitment base" + ); let g1_pt: G1Affine = G1Affine::generator(); let g2_pt: G2Affine = self.params.g2().to_affine(); let neg_s_g2_pt: G2Affine = (-self.params.s_g2()).to_affine(); + + // The G1 base is validated above by reconstructing it from + // `g_lagrange`. Do the same work on the G2 side. Until now `g2` + // and `s_g2` were taken on trust from `params`, and + // `NEG_S_G2_BASE` is the element every soundness guarantee in the + // deployed verifier rests on: the final pairing is + // e(final_com - v*G + x3*pi, G2_BASE) * e(pi, NEG_S_G2_BASE) == 1 + // and anyone who knows the tau behind NEG_S_G2_BASE can forge an + // opening for any statement. + // + // Every other control in this repository -- quotient + // certification, the dual build, generator invariants, the VK + // codehash pin, the trace differential, the replay fixtures -- + // checks SELF-CONSISTENCY. An SRS substituted at build time + // produces a perfectly self-consistent artifact, so all of them + // pass. These asserts are the only build-time control that can + // catch it. (H-1, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md.) + + // 1. G2_BASE must be the canonical generator, for the same reason G1_BASE must + // be: the emitted equation assumes it. + assert_eq!( + g2_pt, + G2Affine::generator(), + "SRS G2 base is not the canonical BLS12-381 G2 generator; the \ + emitted pairing equation would not be the KZG identity" + ); + + // 2. `s_g2` must correspond to the SAME tau that produced `g_lagrange`; see + // `srs_tau_is_consistent`. + assert!( + srs_tau_is_consistent( + g_lagrange, + domain.get_omega(), + self.params.s_g2().to_affine() + ), + "SRS inconsistency: s_g2 does not correspond to the tau that \ + generated g_lagrange. NEG_S_G2_BASE would be emitted from an \ + SRS unrelated to the commitment basis -- i.e. from a key whose \ + toxic waste may be known to whoever supplied it." + ); + + // TODO(deployment): additionally pin the SRS asset by SHA-256 and + // record it, plus the ceremony transcript reference, in + // docs/reference/REPRODUCIBLE_BUILDS.md; scripts/ + // record_srs_provenance.sh produces the hashes. + let g1 = g1_to_u256s(g1_pt); let g2 = g2_to_u256s(g2_pt); let neg_s_g2 = g2_to_u256s(neg_s_g2_pt); @@ -366,6 +497,8 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { Self::transcript_buffer_layout_for_meta(meta, self.num_instances).words; let transcript_end = layout::TRANSCRIPT_BUFFER_START + transcript_words * WORD_BYTES; let pcs_end = layout::PCS_PAIRING_SCRATCH_START + pcs_computation * WORD_BYTES; + let pairing_batch_end = + layout::accumulator::PAIRING_BATCH_PTR + layout::accumulator::PAIRING_BATCH_HASH_BYTES; let final_pairing_end = layout::FINAL_PAIRING_SCRATCH_START + layout::PAIRING_STATIC_WORKING_WORDS * WORD_BYTES; let verifier_return_end = layout::VERIFIER_RETURN_BUFFER_START + WORD_BYTES; @@ -381,8 +514,12 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { transcript_end, // PCS computation scratch pcs_end, + // Accumulator/KZG pairing-batch hash frame. Currently ends where + // the final pairing frame starts, but list it explicitly so the + // bound survives if that derivation changes. + pairing_batch_end, // Pairing: two-pair input frame plus output word, rooted above - // Solidity's reserved memory prefix. + // solc's via-IR spill window. final_pairing_end, // Low-memory return frames. The quotient evaluator's output grows // with the number of simple selector buckets. @@ -414,8 +551,9 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { // midnight-proofs verifiers the dominating run is whichever of the // following is largest: // (a) initial absorbs (vk_digest + committed_pi + num_instances - // + all instance scalars + all phase-1 advices) before the - // first user-phase challenge squeeze (`theta`), or + // + all instance scalars + every advice commitment up to and + // including the first challenge-bearing phase) before the first + // user-phase challenge squeeze (`theta` at the latest), or // (b) the evaluation block (all `num_evals` scalars) absorbed // after the `y` squeeze and before the next squeeze. // @@ -438,6 +576,10 @@ impl<'params, 'meta> VerifierBuildInputs<'params, 'meta> { meta.num_evals, meta.num_point_sets, ); - TranscriptBufferLayout::from_proof_layout(&proof_layout, num_instances) + TranscriptBufferLayout::from_proof_layout( + &proof_layout, + num_instances, + &meta.protocol.num_user_challenges, + ) } } diff --git a/proofs/solidity-verifier/src/test.rs b/proofs/solidity-verifier/src/test.rs index 0c585e179..387b77c21 100644 --- a/proofs/solidity-verifier/src/test.rs +++ b/proofs/solidity-verifier/src/test.rs @@ -30,9 +30,14 @@ use midnight_proofs::{ poly::{commitment::Guard as _, kzg::KZGCommitmentScheme, Rotation}, transcript::{CircuitTranscript, Transcript}, }; +#[cfg(not(feature = "outer-single-h-commitment"))] +use midnight_zk_stdlib::utils::plonk_api::srs_for_test; +#[cfg(feature = "outer-single-h-commitment")] use midnight_zk_stdlib::{ - setup_vk, utils::plonk_api::srs_for_test, MidnightVK, Relation, ZkStdLib, ZkStdLibArch, + cost_model, + utils::plonk_api::{load_srs, SrsSource}, }; +use midnight_zk_stdlib::{setup_vk, MidnightVK, Relation, ZkStdLib, ZkStdLibArch}; use proptest::{ prelude::any, test_runner::{Config as ProptestConfig, TestRunner}, @@ -45,7 +50,8 @@ use ruint::aliases::U256; use sha3::Digest; use crate::{ - compile_solidity, encode_calldata, pinned_solc_available, CallOutcome, Evm, GeneratorConfig, + compile_solidity, compile_solidity_runtime, encode_calldata, pinned_solc_available, + runtime_free_memory_pointer_init, AccumulatorEncoding, CallOutcome, Evm, GeneratorConfig, RenderDiagnostics, RenderOptions, RenderQuotient, RenderVk, SolidityGenerator, FN_SIG_VERIFY_PROOF, }; @@ -62,6 +68,9 @@ type PoseidonVerifierParams = const POSEIDON_K: u32 = 6; /// Environment flag that opts into expensive EVM/Solidity integration tests. const RUN_EVM_TESTS_ENV: &str = "HALO2_SOLIDITY_RUN_EVM_TESTS"; +/// Source for the test SRS, as documented by `zk_stdlib`'s own loader error. +const SRS_DOWNLOAD_URL: &str = + "https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19"; /// Minimal caller used to exercise the production verifier under STATICCALL. const STATICCALL_VERIFIER_HARNESS: &str = r#" // SPDX-License-Identifier: CC0-1.0 @@ -461,7 +470,7 @@ fn lookup_shape_verifier_compiles_with_native_lookup_callback() { }; let circuit = ShapeFuzzCircuit::new(case.spec, case.seed); let mut setup_rng = ChaCha8Rng::seed_from_u64(case.seed ^ 0x5eed_5eed); - let params = PoseidonParams::unsafe_setup(case.k, &mut setup_rng); + let params = shape_fuzz_params(&[(case.spec, case.k)], &mut setup_rng); let vk = keygen_vk_with_k::, _>(¶ms, &circuit, case.k) .unwrap_or_else(|err| panic!("shape fuzz `{}` vk generation failed: {err:?}", case.name)); @@ -630,7 +639,8 @@ fn same_srs_distinct_shape_matrix_rejects_cross_wiring() { ]; let mut setup_rng = ChaCha8Rng::seed_from_u64(0x5a5a_5151); - let params = PoseidonParams::unsafe_setup(cases[0].k, &mut setup_rng); + let shapes: Vec<_> = cases.iter().map(|case| (case.spec, case.k)).collect(); + let params = shape_fuzz_params(&shapes, &mut setup_rng); let fixtures: Vec<_> = cases .iter() .map(|case| build_shape_solidity_case_with_params(¶ms, case)) @@ -803,7 +813,7 @@ fn build_shape_solidity_case_with_params( fn run_supported_shape_fuzz_case(case: &ShapeFuzzCase) -> bool { let circuit = ShapeFuzzCircuit::new(case.spec, case.seed); let mut setup_rng = ChaCha8Rng::seed_from_u64(case.seed ^ 0x5eed_5eed); - let params = PoseidonParams::unsafe_setup(case.k, &mut setup_rng); + let params = shape_fuzz_params(&[(case.spec, case.k)], &mut setup_rng); let vk = keygen_vk_with_k::, _>(¶ms, &circuit, case.k) .unwrap_or_else(|err| panic!("shape fuzz `{}` vk generation failed: {err:?}", case.name)); let pk = keygen_pk(vk, &circuit) @@ -920,13 +930,65 @@ fn generated_shape_fuzz_spec(seed: u64) -> ShapeFuzzSpec { } } +/// Circuit domain size used by the transcript differential shape fuzzer. +#[cfg(feature = "rust-verifier-trace")] +const SHAPE_FUZZ_K: u32 = 5; + +/// Build test parameters covering every supplied `(shape, k)` pair. +/// +/// Under `outer-single-h-commitment` the prover commits to one unsplit quotient +/// polynomial of degree `(n - 1) * quotient_poly_degree`, so a plain +/// `unsafe_setup(k)` is too small and proof generation fails with a `SrsError` +/// long after the VK builds cleanly at `k`. The monomial basis has to be larger +/// than the circuit domain while the Lagrange basis stays at `2^k` -- exactly +/// the recipe documented on `ParamsKZG::downsize_lagrange`. +/// +/// Loading a real SRS is not an option for these circuits: they are generated +/// per seed, so each fixture draws its own toxic secret. Callers that share one +/// parameter set across several shapes pass them all here, and the monomial +/// basis is sized for the most demanding one. +fn shape_fuzz_params(shapes: &[(ShapeFuzzSpec, u32)], rng: &mut ChaCha8Rng) -> PoseidonParams { + let (_, lagrange_k) = shapes.first().copied().expect("at least one shape"); + assert!( + shapes.iter().all(|(_, k)| *k == lagrange_k), + "one parameter set can only serve shapes that share a circuit domain size" + ); + + #[cfg(not(feature = "outer-single-h-commitment"))] + { + PoseidonParams::unsafe_setup(lagrange_k, rng) + } + #[cfg(feature = "outer-single-h-commitment")] + { + let extended_k = shapes + .iter() + .map(|(spec, k)| { + // Configure a throwaway constraint system to learn this shape's + // degree. keygen may compress selectors, which can only lower + // the degree, so the uncompressed value is a safe upper bound. + let mut cs = ConstraintSystem::::default(); + >::configure_with_params(&mut cs, *spec); + // Same exponent `midnight_zk_stdlib::utils::plonk_api::load_srs` + // uses for the single-h monomial basis. + k + ((cs.degree() - 1) as f64).log2().ceil() as u32 + }) + .max() + .expect("at least one shape"); + let mut params = PoseidonParams::unsafe_setup(extended_k, rng); + // Keep the monomial basis extended; shrink only the Lagrange basis back + // to the circuit domain so commitments stay at 2^k. + params.downsize_lagrange(lagrange_k); + params + } +} + #[cfg(feature = "rust-verifier-trace")] fn run_transcript_differential_shape_fuzz_case(seed: u64) { let spec = generated_shape_fuzz_spec(seed); let context = format!("transcript differential seed={seed:#018x} spec={spec:?}"); let circuit = ShapeFuzzCircuit::new(spec, seed); let mut setup_rng = ChaCha8Rng::seed_from_u64(seed ^ 0x7ace_f00d); - let params = PoseidonParams::unsafe_setup(5, &mut setup_rng); + let params = shape_fuzz_params(&[(spec, SHAPE_FUZZ_K)], &mut setup_rng); let vk = keygen_vk_with_k::, _>(¶ms, &circuit, 5) .unwrap_or_else(|err| panic!("{context} vk generation failed: {err:?}")); let pk = keygen_pk(vk, &circuit) @@ -972,10 +1034,9 @@ fn shape_fuzz_inputs_available_for_evm() -> bool { eprintln!("skipping supported-shape circuit fuzz: set {RUN_EVM_TESTS_ENV}=1 to run it"); return false; } - if !solc_available() { - eprintln!("skipping supported-shape circuit fuzz: solc not found"); - return false; - } + // Requested but unusable is a failure, not a skip. See + // `poseidon_inputs_available_for_evm`. + solc_available(); true } @@ -1355,6 +1416,279 @@ fn pinned_quotient_verifier_rejects_wrong_vk_and_quotient_contracts() { ); } +/// The verifier body is wrapped in `assembly ("memory-safe")` while writing +/// absolute addresses, which is a false promise: it never allocates through the +/// free-memory pointer. That annotation is nonetheless load-bearing -- without +/// it the block does not compile (stack too deep) -- and it is what lets solc's +/// via-IR stack-to-memory mover reserve spill slots upward from `0x80`. +/// +/// The generated layout is therefore based above that reservation rather than +/// at `0x80`, so solc's spill slots and the verifier's own memory are disjoint +/// by construction instead of by a liveness coincidence. The reservation size +/// is not fixed -- observed values range from `0x80` to `0x8e0` depending on +/// the circuit, the solc release, and the optimizer schedule -- so assert the +/// property against real compiled bytecode rather than assuming it holds. +/// +/// `VerifierMemoryLayout::validate()` enforces the complementary bound for the +/// generated layout: every generated region starts at or above +/// `LOW_MEMORY_SCRATCH_START`, so `reserved_end <= LOW_MEMORY_SCRATCH_START` +/// here proves full disjointness. Check both verifier and quotient evaluator +/// runtimes, because each via-IR compilation unit receives its own independent +/// spill reservation. The accumulator-bearing variants are checked too, +/// because their FinalPairing pairing-batch block adds frames and live values +/// the property fixture never renders. +#[test] +fn compiled_memoryguard_does_not_overlap_generated_layout() { + if !poseidon_inputs_available_for_evm() { + return; + } + + fn assert_memoryguard_clears_generated_layout(name: &str, source: &str) { + let runtime = compile_solidity_runtime(source); + let reserved_end = runtime_free_memory_pointer_init(&runtime).unwrap_or_else(|| { + panic!("{name}: could not read the free-memory-pointer prologue from runtime bytecode") + }); + assert!( + reserved_end <= crate::lowering::layout::LOW_MEMORY_SCRATCH_START, + "{name}: solc reserved [0x80, {reserved_end:#x}) for via-IR spill slots, which \ + overlaps the generated verifier layout based at {:#x}. A live spill slot can then \ + sit across a verifier write, silently corrupting a challenge or pairing input. \ + Raise LOW_MEMORY_SCRATCH_START above {reserved_end:#x}.", + crate::lowering::layout::LOW_MEMORY_SCRATCH_START + ); + } + + let fixture = create_property_poseidon_fixture(); + for (name, source) in [ + ("embedded", fixture.embedded_verifier_solidity.as_str()), + ("separate", fixture.separate_verifier_solidity.as_str()), + ("quotient", fixture.quotient_verifier_solidity.as_str()), + ( + "quotient evaluator", + fixture.quotient_evaluator_solidity.as_str(), + ), + ( + "trace quotient evaluator", + fixture.trace_quotient_evaluator_solidity.as_str(), + ), + ] { + assert_memoryguard_clears_generated_layout(name, source); + } + + for (name, _, artifacts, quotient_evaluator) in render_accumulator_verifier_variants() { + assert_memoryguard_clears_generated_layout(name, &artifacts.verifier); + assert_memoryguard_clears_generated_layout( + &format!("{name} quotient evaluator"), + "ient_evaluator, + ); + } +} + +/// `Halo2VerifyingKey` is size-checked at render time by +/// `validate_payload_layout`, because a data contract's runtime length is known +/// before compilation. The verifier's is not -- it only exists once solc has +/// run -- so nothing bounded it, and the revm harness deliberately sets +/// `limit_contract_code_size = usize::MAX` (see `evm.rs`), meaning an oversized +/// verifier would pass the whole suite and then fail to deploy on any EIP-170 +/// chain. Check the compiled artifact directly. +#[test] +fn compiled_verifier_runtime_fits_the_eip170_limit() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let limit = crate::lowering::render::EIP_170_MAX_RUNTIME_BYTES; + let fixture = create_property_poseidon_fixture(); + for (name, source) in [ + ("embedded", fixture.embedded_verifier_solidity.as_str()), + ("separate", fixture.separate_verifier_solidity.as_str()), + ("quotient", fixture.quotient_verifier_solidity.as_str()), + ("vk", fixture.vk_solidity.as_str()), + ] { + let runtime_len = compile_solidity_runtime(source).len(); + assert!( + runtime_len <= limit, + "{name}: compiled runtime is {runtime_len} bytes, over the EIP-170 limit of \ + {limit}; this contract cannot be deployed on mainnet or any \ + EIP-170 chain. Note the revm harness lifts this cap, so no other test catches it." + ); + } +} + +/// Extract one rendered Yul function (signature through matching close brace) +/// from generated verifier source. The rendered helpers contain no string +/// literals, so plain brace counting is sufficient. +fn extract_yul_function<'a>(source: &'a str, signature_prefix: &str) -> &'a str { + let start = source + .find(signature_prefix) + .unwrap_or_else(|| panic!("rendered source should define {signature_prefix}")); + let tail = &source[start..]; + let open = tail.find('{').expect("function definition must open a brace"); + let mut depth = 0usize; + for (idx, byte) in tail.bytes().enumerate().skip(open) { + match byte { + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + return &tail[..=idx]; + } + } + _ => {} + } + } + panic!("unbalanced braces while extracting {signature_prefix}"); +} + +/// Test-only contract that runs the rendered `batch_invert` helper on +/// caller-chosen memory words. Raw calldata is `n || r || n words`; raw +/// returndata is `success flag || the n (possibly inverted) words`. +fn batch_invert_harness_source(verifier_solidity: &str) -> String { + let batch_invert = extract_yul_function( + verifier_solidity, + "function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret", + ); + let modexp_gas = crate::lowering::layout::gas::modexp_gas_word_frame(); + format!( + r#"// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.24; + +contract BatchInvertHarness {{ + // The extracted helper forwards the pinned modexp bound (the maximum over + // the EIP-2565 and EIP-7883 schedules, MF-1); mirror the generated + // constant it references. + uint256 internal constant MODEXP_GAS = {modexp_gas}; + + fallback() external {{ + assembly {{ + {batch_invert} + + let n := calldataload(0x00) + let r := calldataload(0x20) + let base := 0x1000 + calldatacopy(base, 0x40, mul(n, 0x20)) + // MF-4: batch_invert now also reports WHY it failed (a failed + // modexp staticcall vs a rejected denominator). This harness only + // asserts fail-closed behaviour, so it keeps the boolean and + // discards the cause -- but it must still destructure both values + // or the extracted helper does not compile. + let ok, precompile_failed := batch_invert(1, base, add(base, mul(n, 0x20)), 0x8000, r) + mstore(0x80, ok) + pop(precompile_failed) + mcopy(0xa0, base, mul(n, 0x20)) + return(0x80, add(0x20, mul(n, 0x20))) + }} + }} +}} +"# + ) +} + +/// Execute the rendered `batch_invert` helper against adversarial words. +/// +/// The template greps in `lowering/tests.rs` pin the guard text; this pins +/// the behavior: the singleton and general paths must both fail closed on +/// words outside the canonical range (`x >= r`, including invertible +/// residues, `x = r`, and literal zero) without touching the input run, +/// so accept/reject semantics never depend on batch length. Canonical +/// batches must produce exactly the native inverses. +#[test] +fn batch_invert_fails_closed_on_noncanonical_words_in_all_paths() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let harness = batch_invert_harness_source(&fixture.embedded_verifier_solidity); + let mut evm = Evm::default(); + let address = evm.create(compile_solidity(&harness)); + + let r = fr_modulus_u256(); + let mut run = |elems: &[U256]| -> (bool, Vec) { + let mut calldata = Vec::with_capacity((2 + elems.len()) * 0x20); + calldata.extend_from_slice(&U256::from(elems.len()).to_be_bytes::<0x20>()); + calldata.extend_from_slice(&r.to_be_bytes::<0x20>()); + for elem in elems { + calldata.extend_from_slice(&elem.to_be_bytes::<0x20>()); + } + match evm.try_call(address, calldata) { + CallOutcome::Success { output, .. } => { + assert_eq!( + output.len(), + (1 + elems.len()) * 0x20, + "harness returndata shape" + ); + let flag = U256::try_from_be_slice(&output[..0x20]).unwrap(); + assert!(flag <= U256::from(1), "success flag must be boolean"); + let words = output[0x20..] + .chunks_exact(0x20) + .map(|word| U256::try_from_be_slice(word).unwrap()) + .collect(); + (flag == U256::from(1), words) + } + outcome => panic!("harness must not revert or halt: {outcome:?}"), + } + }; + + let word = |value: u64| U256::from(value); + let inv = + |value: u64| crate::lowering::encoding::fe_to_u256::(F::from(value).invert().unwrap()); + + // Canonical batches succeed and invert every element in place; the + // lengths cover the empty, singleton, two-element, and looped general + // paths. + let (ok, out) = run(&[]); + assert!(ok, "empty batch must be a no-op success"); + assert!(out.is_empty()); + for elems in [vec![7u64], vec![2, 3], vec![1, 2, 3, 5, 7]] { + let input: Vec = elems.iter().copied().map(word).collect(); + let (ok, out) = run(&input); + assert!(ok, "canonical batch of {} must succeed", elems.len()); + let expected: Vec = elems.iter().copied().map(inv).collect(); + assert_eq!( + out, + expected, + "batch of {} must produce native inverses", + elems.len() + ); + } + + // Every rejection leaves the input run untouched: zero and anything + // congruent to zero mod r has no inverse, and non-canonical words with + // invertible residues (x = r + 5, 2^256 - 1) must fail closed in both + // paths rather than being reduced by mulmod. The three r + 5 positions + // hit the general path's first-element, loop, and final-element guards. + let r_plus_5 = r + word(5); + for (name, elems) in [ + ("singleton literal zero", vec![U256::ZERO]), + ("singleton x = r", vec![r]), + ("singleton x = r + 5", vec![r_plus_5]), + ("singleton x = 2^256 - 1", vec![U256::MAX]), + ("general literal zero", vec![word(2), U256::ZERO, word(3)]), + ("general x = r", vec![word(2), r, word(3)]), + ( + "general first element x = r + 5", + vec![r_plus_5, word(2), word(3)], + ), + ( + "general loop element x = r + 5", + vec![word(2), r_plus_5, word(3)], + ), + ( + "general final element x = r + 5", + vec![word(2), word(3), r_plus_5], + ), + ( + "general two-element x = 2^256 - 1", + vec![word(2), U256::MAX], + ), + ] { + let (ok, out) = run(&elems); + assert!(!ok, "{name} must fail closed"); + assert_eq!(out, elems, "{name} must leave the input words untouched"); + } +} + #[test] fn verifier_constructor_rejects_missing_or_mismatched_eip2537_precompiles() { if !poseidon_inputs_available_for_evm() { @@ -1365,18 +1699,18 @@ fn verifier_constructor_rejects_missing_or_mismatched_eip2537_precompiles() { for (name, needle, replacement) in [ ( "missing G1ADD precompile", - "staticcall(gas(), 0x0b", - "staticcall(gas(), 0x12", + "staticcall(G1ADD_GAS, 0x0b", + "staticcall(G1ADD_GAS, 0x12", ), ( "G1MSM routed to G1ADD", - "staticcall(gas(), 0x0c", - "staticcall(gas(), 0x0b", + "staticcall(G1MSM_GAS_1PAIR, 0x0c", + "staticcall(G1MSM_GAS_1PAIR, 0x0b", ), ( "pairing routed to G1MSM", - "staticcall(gas(), 0x0f", - "staticcall(gas(), 0x0c", + "staticcall(PAIRING_GAS_2PAIR, 0x0f", + "staticcall(PAIRING_GAS_2PAIR, 0x0c", ), ] { let verifier_solidity = replace_required_precompile_staticcall( @@ -1393,9 +1727,75 @@ fn verifier_constructor_rejects_missing_or_mismatched_eip2537_precompiles() { } } +/// MF-1: the constructor's modexp probe must actually REJECT a bound below the +/// chain's price, not merely be present in the rendered source. +/// +/// This is the property the whole MF-1 fix rests on. A `staticcall` forwards a +/// fixed amount, so an under-priced `MODEXP_GAS` does not degrade -- the +/// precompile runs out of gas and every proof reverts. Before the fix there was +/// no modexp probe at all, so such an artifact DEPLOYED CLEANLY and only failed +/// on first use; the probe exists to turn that into a failed deployment. +/// +/// The mutation lowers the bound to one gas below the EIP-2565 price this +/// harness's revm charges (1354), which is exactly the shape of the real bug: +/// the shipped 1360 sat below the EIP-7883 price of 4064. It cannot be tested +/// by repricing revm instead -- the pinned revm 19 exposes `SpecId::OSAKA` but +/// still prices modexp with `berlin_run` -- so the bound is moved rather than +/// the schedule. Positive control: every other EVM test in this file deploys +/// the same fixture unmutated. +#[test] +fn constructor_rejects_a_modexp_bound_below_the_chain_price() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let rendered_bound = crate::lowering::layout::gas::modexp_gas_word_frame(); + let needle = format!("MODEXP_GAS = {rendered_bound};"); + // One gas below what revm's Berlin/EIP-2565 modexp charges for the + // verifier's 32/32/32 frame with a 255-bit exponent: max(200, 16*254/3). + let underpriced = (16 * 254 / 3) - 1; + + let fixture = create_property_poseidon_fixture(); + let verifier_solidity = { + assert!( + fixture.quotient_verifier_solidity.contains(&needle), + "rendered verifier should pin the generated modexp bound ({needle})" + ); + fixture.quotient_verifier_solidity.replacen( + &needle, + &format!("MODEXP_GAS = {underpriced};"), + 1, + ) + }; + + assert_pinned_quotient_constructor_rejects( + &verifier_solidity, + &fixture.vk_solidity, + &fixture.quotient_evaluator_solidity, + "modexp bound below the chain's price", + ); + + // Positive control, so the assertion above cannot pass vacuously: the same + // fixture, same compiler, same EVM, differing ONLY in that constant must + // construct successfully. Without this, a rejection caused by anything + // else in the pipeline would read as the probe working. + let mut evm = Evm::default(); + let vk_address = evm.create(compile_solidity(&fixture.vk_solidity)); + let quotient_address = evm.create(compile_solidity(&fixture.quotient_evaluator_solidity)); + evm.create_with_two_address_args( + compile_solidity(&fixture.quotient_verifier_solidity), + vk_address, + quotient_address, + ); +} + #[test] fn production_renders_do_not_emit_gas_checkpoints() { - if crate::SOLIDITY_GAS_CHECKPOINTS_ENABLED { + // The fixture's base variants follow `RenderDiagnostics::default()`, so a + // `solidity-trace` build makes them emit LOG1 and drop `view` for the same + // reason a gas-checkpoint build does. Both are intended diagnostic shapes, + // not production renders, so neither can satisfy the assertions below. + if crate::SOLIDITY_GAS_CHECKPOINTS_ENABLED || crate::SOLIDITY_TRACE_ENABLED { return; } if !poseidon_inputs_available_for_evm() { @@ -1428,6 +1828,14 @@ fn production_renders_do_not_emit_gas_checkpoints() { #[test] fn production_separate_verifier_accepts_valid_proof_under_staticcall() { + // The fixture renders its base variants with `RenderDiagnostics::default()`, + // which follows the crate's feature flags. A trace or gas-checkpoint build + // emits LOG1 and is deliberately not `view`, so it cannot be STATICCALL-ed + // -- that is the documented contract, not a regression. Skip on those + // builds, matching `production_renders_do_not_emit_gas_checkpoints`. + if crate::SOLIDITY_TRACE_ENABLED || crate::SOLIDITY_GAS_CHECKPOINTS_ENABLED { + return; + } if !poseidon_inputs_available_for_evm() { return; } @@ -1485,6 +1893,212 @@ fn compile_solidity_is_deterministic_for_same_source() { assert_eq!(bytecode_a, bytecode_b); } +/// The accumulator fixed-base scalar tail must match the verifying key. +/// +/// `fixed_scalar_count` is derived from `num_instances`, but the bases those +/// scalars multiply are generated as `fixed_comm_mptr + i * 0x80` from the VK. +/// A tail longer than the VK's fixed-commitment count used to render fine and +/// silently emit base pointers past that region, aliasing permutation +/// commitments -- and beyond them arbitrary VK payload words -- as accumulator +/// G1 bases. +#[test] +fn accumulator_fixed_base_tail_must_match_verifying_key() { + if !poseidon_inputs_available_for_evm() { + return; + } + + let srs_dir = srs_dir(); + env::set_var("SRS_DIR", &srs_dir); + let relation = PoseidonExample; + let srs = poseidon_srs_for_test(&relation); + let vk = setup_vk(&srs, &relation); + + let num_fixed_comms = vk.vk().fixed_commitments().len(); + let num_permutation_comms = vk.vk().permutation().commitments().len(); + let collapsed = AccumulatorEncoding::FULLY_COLLAPSED_PUBLIC_INPUT_WORDS; + // `-G` plus every permutation commitment, then up to one scalar per fixed + // commitment. + let min_tail = 1 + num_permutation_comms; + let max_tail = min_tail + num_fixed_comms; + + // A tail three scalars longer than the VK can supply bases for. Before the + // guard this rendered a verifier whose last three "fixed bases" were + // permutation commitments. + let err = SolidityGenerator::try_new( + &srs, + vk.vk(), + GeneratorConfig::new(collapsed + max_tail + 3, 1) + .with_accumulator(AccumulatorEncoding::new(0, 7, 56)), + ) + .expect_err("oversized accumulator fixed-base tail should be rejected"); + assert!( + matches!( + err, + crate::GeneratorError::AccumulatorFixedBaseTailMismatch { + fixed_scalar_count, + max_fixed_scalar_count, + .. + } if fixed_scalar_count == max_tail + 3 && max_fixed_scalar_count == max_tail + ), + "unexpected error for oversized tail: {err}" + ); + + // A tail too short to cover `-G` plus the permutation commitments would + // underflow the base count in the artifact emitter. + SolidityGenerator::try_new( + &srs, + vk.vk(), + GeneratorConfig::new(collapsed + min_tail - 1, 1) + .with_accumulator(AccumulatorEncoding::new(0, 7, 56)), + ) + .expect_err("undersized accumulator fixed-base tail should be rejected"); + + // Supported shapes still build: no tail at all, the full base set, and a + // prefix of the fixed commitments in between (every pointer stays inside + // the fixed-commitment region). + for num_instances in [ + collapsed, + collapsed + min_tail, + collapsed + max_tail, + collapsed + (min_tail + max_tail) / 2, + ] { + SolidityGenerator::try_new( + &srs, + vk.vk(), + GeneratorConfig::new(num_instances, 1) + .with_accumulator(AccumulatorEncoding::new(0, 7, 56)), + ) + .unwrap_or_else(|err| panic!("supported accumulator tail should build: {err}")); + } +} + +/// Render the three accumulator-bearing verifier variants over the Poseidon +/// fixture VK: fully collapsed, fixed-base scalar tail, and point-pair +/// encodings. Returns `(name, has_carried_scalars, artifacts, evaluator)` per +/// variant. +fn render_accumulator_verifier_variants( +) -> Vec<(&'static str, bool, crate::RenderedArtifacts, String)> { + let srs_dir = srs_dir(); + env::set_var("SRS_DIR", &srs_dir); + let relation = PoseidonExample; + let srs = poseidon_srs_for_test(&relation); + let vk = setup_vk(&srs, &relation); + + // A fully collapsed accumulator occupies FULLY_COLLAPSED_PUBLIC_INPUT_WORDS + // public inputs and has no fixed-base scalar tail. + let collapsed_words = AccumulatorEncoding::FULLY_COLLAPSED_PUBLIC_INPUT_WORDS; + // The partially collapsed form appends one scalar per generated base: + // `-G`, then every fixed commitment, then every permutation commitment. + let num_fixed_comms = vk.vk().fixed_commitments().len(); + let num_perm_comms = vk.vk().permutation().commitments().len(); + let tail_words = 1 + num_fixed_comms + num_perm_comms; + + let variants = [ + ( + "fully collapsed accumulator", + collapsed_words, + AccumulatorEncoding::new(0, 7, 56), + true, + ), + ( + "accumulator with fixed-base scalar tail", + collapsed_words + tail_words, + AccumulatorEncoding::new(0, 7, 56), + true, + ), + // Point-pair encodings carry no explicit scalars -- both are implicit + // one -- so no calldata scalar is read and none is range-checked. + ( + "point-pair accumulator", + AccumulatorEncoding::POINT_PAIR_PUBLIC_INPUT_WORDS, + AccumulatorEncoding::point_pair(0, 7, 56), + false, + ), + ]; + + variants + .into_iter() + .map(|(name, num_instances, acc, has_carried_scalars)| { + let generator = SolidityGenerator::new( + &srs, + vk.vk(), + GeneratorConfig::new(num_instances, 1).with_accumulator(acc), + ); + let artifacts = generator + .render(RenderOptions { + vk: RenderVk::Separate, + ..RenderOptions::default() + }) + .unwrap_or_else(|err| panic!("{name} should render: {err}")); + let quotient_evaluator = generator + .render_quotient_evaluator(RenderDiagnostics::default()) + .unwrap_or_else(|err| panic!("{name} quotient evaluator should render: {err}")); + (name, has_carried_scalars, artifacts, quotient_evaluator) + }) + .collect() +} + +/// Compile the accumulator render arm. +/// +/// No production fixture enables `with_accumulator`, so before this test the +/// whole `{%- if self.expected_has_accumulator %}` branch of +/// AccumulatorHelpers.yul -- the limb decoder, the pre-transcript +/// public-accumulator MSM, and the fixed-base scalar tail -- was never handed +/// to solc by the default gate. Only the opt-in `ivc_keccak_solidity` bench +/// (k = 20, release, external SRS assets) rendered it, so a Yul syntax error +/// or a solc stack-depth regression in that branch could reach a release +/// unnoticed. +/// +/// This does not execute the accumulator logic against a real recursive proof +/// -- that still needs a decider circuit carrying a genuine accumulator in its +/// public inputs. It does guarantee the branch compiles, and it pins the +/// canonicality checks on the scalars the helper feeds to G1MSM. +#[test] +fn accumulator_verifier_variants_compile_with_pinned_solc() { + if !poseidon_inputs_available_for_evm() { + return; + } + + for (name, has_carried_scalars, artifacts, _) in render_accumulator_verifier_variants() { + let verifier = artifacts.verifier; + assert!( + verifier.contains("function validate_public_accumulator"), + "{name} should render the accumulator helper" + ); + // The helper runs before the transcript loop that rejects + // non-canonical instance words, and EIP-2537 G1MSM reduces scalars mod + // r implicitly, so it must reject `s >= r` itself. + if has_carried_scalars { + for required in ["lt(lhs_scalar, r)", "lt(rhs_scalar, r)"] { + assert!( + verifier.contains(required), + "{name} should range-check accumulator scalars: {required}" + ); + } + } else { + assert!( + !verifier.contains("lt(lhs_scalar, r)"), + "{name} carries no explicit scalars, so none should be read or checked" + ); + } + + for (label, source) in [ + (name, verifier.as_str()), + ( + "accumulator VK", + artifacts.verifying_key.as_deref().expect("separate render includes VK"), + ), + ] { + let bytecode = std::panic::catch_unwind(AssertUnwindSafe(|| compile_solidity(source))) + .unwrap_or_else(|_| panic!("{label} should compile under the pinned solc")); + assert!( + !bytecode.is_empty(), + "{label} should compile to non-empty bytecode" + ); + } + } +} + #[test] fn poseidon_verifier_variants_compile_with_pinned_solc() { if !poseidon_inputs_available_for_evm() { @@ -1867,7 +2481,7 @@ fn load_poseidon_vk_sources_fixture() -> PoseidonVkSourcesFixture { env::set_var("SRS_DIR", &srs_dir); let relation = PoseidonExample; - let srs = srs_for_test(&relation, Some(POSEIDON_K)); + let srs = poseidon_srs_for_test(&relation); let vk = setup_vk(&srs, &relation); assert_eq!(vk.k() as u32, POSEIDON_K, "unexpected Poseidon VK k"); @@ -1899,7 +2513,7 @@ fn load_property_poseidon_fixture() -> PropertyPoseidonFixture { env::set_var("SRS_DIR", &srs_dir); let relation = PoseidonExample; - let srs = srs_for_test(&relation, Some(POSEIDON_K)); + let srs = poseidon_srs_for_test(&relation); let vk = setup_vk(&srs, &relation); assert_eq!(vk.k() as u32, POSEIDON_K, "unexpected Poseidon VK k"); @@ -1999,6 +2613,7 @@ fn load_property_poseidon_fixture() -> PropertyPoseidonFixture { trace: true, ..RenderDiagnostics::default() }, + provenance: None, }) .expect("trace pinned render with quotient evaluator"); let trace_quotient_verifier_solidity = trace_quotient_artifacts.verifier; @@ -2322,6 +2937,30 @@ fn verifier_rejects_when_x_is_forced_to_domain_root() { call_deployed_verifier(&mut evm, &fixture.proof, &fixture.instances), "verifier with x forced to domain root should hit zero Lagrange denominator", ); + + // MF-4: pin WHICH rejection this is, not just that it rejects. A zero + // Lagrange denominator means the squeezed x coincided with a domain point + // -- a transcript event, so ProofRejected. It used to surface as + // PrecompileFailed, which sends an incident responder to inspect the node + // when the proof was the cause. This is the only live path that reaches + // that branch, so without this assertion the split is pinned in template + // text but never observed executing. + let selector = { + use sha3::{Digest, Keccak256}; + let d = Keccak256::digest(b"ProofRejected()"); + vec![d[0], d[1], d[2], d[3]] + }; + match evm.evm.try_call_with_gas( + evm.verifier_address, + encode_calldata(&fixture.proof, &fixture.instances), + 30_000_000, + ) { + CallOutcome::Revert { output, .. } => assert_eq!( + output, selector, + "a zero Lagrange denominator must report ProofRejected, not a precompile fault" + ), + outcome => panic!("forced-domain-root verifier did not revert: {outcome:?}"), + } } #[test] @@ -2442,6 +3081,177 @@ fn every_proof_g1_rejects_off_curve_coordinates() { } } +/// M-2 (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): a rejecting EIP-2537 +/// precompile consumes ALL gas supplied to the STATICCALL, so before the +/// generated exact gas bounds a canonical-but-off-curve proof point burned +/// 63/64 of the transaction budget (measured 29.5M of a 30M limit). With the +/// bounds in place, rejecting a malformed point must cost no more than an +/// honest verification: the failing run is a prefix of the honest run plus +/// one precompile call whose forwarded gas is capped at its scheduled cost. +#[test] +fn malformed_proof_point_rejects_with_bounded_gas() { + const GAS_LIMIT: u64 = 30_000_000; + + if !poseidon_inputs_available_for_evm() { + return; + } + + let fixture = create_property_poseidon_fixture(); + let layout = proof_g1_layout(&fixture); + let solidity_bad = eip2537_padded_off_curve_g1_bytes(); + + let mut deployed = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut deployed, &fixture, &fixture.proof, "valid proof") { + return; + } + let honest_gas = match deployed.evm.try_call_with_gas( + deployed.verifier_address, + encode_calldata(&fixture.proof, &fixture.instances), + GAS_LIMIT, + ) { + CallOutcome::Success { gas_used, .. } => gas_used, + outcome => panic!("baseline honest verification failed: {outcome:?}"), + }; + + for (idx, repacked_offset) in layout.repacked_offsets.iter().copied().enumerate() { + let mut bad_solidity = fixture.proof.clone(); + bad_solidity[repacked_offset..repacked_offset + 128].copy_from_slice(&solidity_bad); + let gas_used = match deployed.evm.try_call_with_gas( + deployed.verifier_address, + encode_calldata(&bad_solidity, &fixture.instances), + GAS_LIMIT, + ) { + CallOutcome::Revert { gas_used, .. } | CallOutcome::Halt { gas_used, .. } => gas_used, + CallOutcome::Success { output, .. } => { + // Rejection-by-return-false also must not burn the budget. + assert_eq!( + output.last().copied(), + Some(0), + "off-curve G1 idx={idx} unexpectedly verified" + ); + continue; + } + }; + assert!( + gas_used <= honest_gas, + "rejecting off-curve G1 idx={idx} repacked_offset={repacked_offset} burned \ + {gas_used} gas, more than the honest verification's {honest_gas}: a \ + precompile call site is forwarding more than its scheduled cost" + ); + } +} + +/// P4 (L-3, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): rejections revert +/// with a typed 4-byte custom-error selector so integrators can distinguish +/// failure classes. Decode three representative classes end-to-end. +#[test] +fn typed_errors_identify_rejection_classes() { + use sha3::{Digest, Keccak256}; + + if !poseidon_inputs_available_for_evm() { + return; + } + let selector = |sig: &str| { + let d = Keccak256::digest(sig.as_bytes()); + vec![d[0], d[1], d[2], d[3]] + }; + + let fixture = create_property_poseidon_fixture(); + let mut deployed = deployed_separate_verifier(&fixture); + if !deployed_call_accepts(&mut deployed, &fixture, &fixture.proof, "valid proof") { + return; + } + + // (a) Trailing calldata byte -> BadCalldataShape (exact calldatasize pin; + // documented ERC-2771 incompatibility). + let mut trailing = encode_calldata(&fixture.proof, &fixture.instances); + trailing.push(0); + // (b) Non-canonical public instance (r) -> NonCanonicalScalar. + let mut bad_instance = encode_calldata(&fixture.proof, &fixture.instances); + let len = bad_instance.len(); + bad_instance[len - 32..].copy_from_slice( + &hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001") + .expect("BLS12-381 r"), + ); + // (c) Off-curve proof point -> BadPointEncoding is checked only for pad / + // range violations; a canonical-but-off-curve point fails at the MSM, so + // expect PrecompileFailed or ProofRejected there instead. Use a pad-byte + // violation for the deterministic BadPointEncoding class. + let layout = proof_g1_layout(&fixture); + let mut bad_pad = fixture.proof.clone(); + bad_pad[layout.repacked_offsets[0]] = 1; + let bad_pad = encode_calldata(&bad_pad, &fixture.instances); + + for (name, calldata, expected_sig) in [ + ("trailing calldata byte", trailing, "BadCalldataShape()"), + ( + "non-canonical instance", + bad_instance, + "NonCanonicalScalar()", + ), + ("pad-byte violation", bad_pad, "BadPointEncoding()"), + ] { + match deployed.evm.try_call_with_gas(deployed.verifier_address, calldata, 30_000_000) { + CallOutcome::Revert { output, .. } => { + assert_eq!( + output, + selector(expected_sig), + "{name} must revert with {expected_sig}" + ); + } + outcome => panic!("{name} did not revert: {outcome:?}"), + } + } +} + +/// H-1 provenance (docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): the +/// production Midnight SRS assets must bind their `s_g2` — the element the +/// deployed verifier's `NEG_S_G2_BASE` is derived from — to the same tau +/// that generated their Lagrange basis. The build-time assert in +/// `src/lowering/vk.rs` runs this check on whatever `params` a build was +/// handed; this test runs it directly against the distributed asset files, +/// so a corrupted or substituted download fails here even before any build. +#[test] +fn midnight_srs_assets_bind_s_g2_to_lagrange_tau() { + use ff::PrimeField; + use midnight_zk_stdlib::utils::plonk_api::{load_srs, SrsSource}; + + if !env_flag_enabled(RUN_EVM_TESTS_ENV) { + eprintln!("skipping Midnight SRS tau-binding check: set {RUN_EVM_TESTS_ENV}=1 to run it"); + return; + } + let srs_dir = srs_dir(); + env::set_var("SRS_DIR", &srs_dir); + + let mut checked = 0usize; + for (asset, k) in [("midnight-srs-2p19", 19u32), ("midnight-srs-2p20", 20u32)] { + if !PathBuf::from(&srs_dir).join(asset).exists() { + eprintln!("skipping {asset}: not present under {srs_dir}"); + continue; + } + let params = load_srs(SrsSource::Midnight, k, 2); + // omega for the 2^k evaluation domain the Lagrange basis is over. + let omega = + ::ROOT_OF_UNITY.pow_vartime([1u64 << (::S - k)]); + assert!( + crate::lowering::vk::srs_tau_is_consistent( + params.g_lagrange(), + omega, + params.s_g2().to_affine(), + ), + "{asset}: s_g2 does not correspond to the tau underlying g_lagrange; \ + the asset is corrupted or was substituted" + ); + checked += 1; + } + if checked == 0 { + eprintln!( + "no midnight-srs assets found under {srs_dir}; \ + fetch them with scripts/run_ivc_bench.sh or record_srs_provenance.sh" + ); + } +} + /// Produce a native Poseidon proof and verify it before Solidity tests use it. fn generate_poseidon_proof( srs: &PoseidonParams, @@ -3251,7 +4061,9 @@ fn assert_rendered_reader_matches_proof_layout( "generated proof reader proof_cptr increments drifted from ProofCalldataLayout" ); assert!( - solidity.contains("if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) }"), + solidity.contains( + "if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) }" + ), "generated proof reader must fail closed if proof_cptr drifts" ); } @@ -3938,13 +4750,13 @@ fn poseidon_inputs_available_for_evm() -> bool { eprintln!("skipping Poseidon Solidity property test: set {RUN_EVM_TESTS_ENV}=1 to run it"); return false; } - if !poseidon_srs_available() { - return false; - } - if !solc_available() { - eprintln!("skipping Poseidon Solidity property test: solc not found"); - return false; - } + // Past this point the gate was explicitly requested, so a missing + // prerequisite is a failure rather than a skip. Returning `false` here + // would report a green run that compiled no Solidity and executed no + // proof -- the failure mode that let the rendered fixture artifacts drift + // out of date across several commits without any test noticing. + poseidon_srs_available(); + solc_available(); true } @@ -3960,25 +4772,73 @@ fn env_flag_enabled(name: &str) -> bool { .unwrap_or(false) } -/// Return whether the Poseidon test SRS can be found on disk. -fn poseidon_srs_available() -> bool { +/// Load the Poseidon test SRS for the feature set this crate was built with. +/// +/// `outer-single-h-commitment` forwards `single-h-commitment` to +/// `midnight-proofs` but deliberately *not* to `midnight-zk-stdlib` (see the +/// feature comment in `Cargo.toml`: recursive proofs checked inside the decider +/// circuit stay on the multi-limb layout). The consequence is that +/// `srs_for_test` cannot be used under that feature: it calls `load_srs`, which +/// decides whether to extend the monomial basis from *zk-stdlib's own* +/// `single-h-commitment` setting. With the feature off there, `load_srs` +/// returns a plain `2^k`-element SRS, while the prover -- compiled from +/// `midnight-proofs` *with* the feature -- commits to one full-degree quotient +/// polynomial and needs `k + ceil(log2(cs_degree - 1))` monomial powers. The +/// mismatch surfaces as `SrsError(64, 252)` at proof generation, i.e. long +/// after the VK builds cleanly at `k`. +/// +/// Mirror `load_srs`'s single-h branch here instead, so `--all-features` is a +/// working configuration rather than one that fails inside the prover. +/// `tests/ivc_keccak_solidity.rs` already does the same thing for the decider +/// proof (`outer_single_h_extended_srs_k`); this is that pattern applied to the +/// Poseidon property fixtures. +fn poseidon_srs_for_test(relation: &PoseidonExample) -> PoseidonParams { + #[cfg(not(feature = "outer-single-h-commitment"))] + { + srs_for_test(relation, Some(POSEIDON_K)) + } + #[cfg(feature = "outer-single-h-commitment")] + { + let cs_degree = cost_model(relation, Some(POSEIDON_K)).max_deg; + // Same exponent `load_srs` uses: enough monomial powers to hold the + // unsplit quotient polynomial. + let extended_k = POSEIDON_K + ((cs_degree - 1) as f64).log2().ceil() as u32; + let base = load_srs(SrsSource::Filecoin, POSEIDON_K, cs_degree); + let extended = load_srs(SrsSource::Filecoin, extended_k, cs_degree); + base.with_extended_monomial(extended) + } +} + +/// Require the Poseidon test SRS on disk. +/// +/// Only called once the EVM gate has been explicitly requested, so a missing +/// asset panics with fetch instructions instead of silently skipping. +fn poseidon_srs_available() { let srs_dir = PathBuf::from(srs_dir()); let exact_srs_path = srs_dir.join(format!("bls_filecoin_2p{POSEIDON_K}")); let fallback_srs_path = srs_dir.join("bls_filecoin_2p19"); - if !exact_srs_path.exists() && !fallback_srs_path.exists() { - eprintln!( - "skipping Poseidon Solidity property test: SRS not found at {} or {}", - exact_srs_path.display(), - fallback_srs_path.display() - ); - return false; - } - true + assert!( + exact_srs_path.exists() || fallback_srs_path.exists(), + "{RUN_EVM_TESTS_ENV}=1 requires the test SRS, but it was not found at {} or {}.\n\ + Fetch it with:\n curl -L -o {} {SRS_DOWNLOAD_URL}\n\ + or point SRS_DIR at an existing copy.", + exact_srs_path.display(), + fallback_srs_path.display(), + fallback_srs_path.display(), + ); } -/// Return whether the configured pinned solc is available. -fn solc_available() -> bool { - pinned_solc_available() +/// Require the pinned solc. +/// +/// Same contract as [`poseidon_srs_available`]: loud once the gate is on. +fn solc_available() { + assert!( + pinned_solc_available(), + "{RUN_EVM_TESTS_ENV}=1 requires solc {}, which was not found or did not match.\n\ + Install it, point SOLC at the binary, or set {}=1 to accept another version.", + crate::PINNED_SOLC_VERSION, + crate::ALLOW_UNPINNED_SOLC_ENV, + ); } /// Resolve the SRS directory used by fixture setup. diff --git a/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2Verifier.sol b/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2Verifier.sol index c0bd92500..aa0e9cc7d 100644 --- a/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2Verifier.sol +++ b/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,34 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. @@ -43,7 +82,7 @@ contract Halo2Verifier { // EXPECTED_VK_PAYLOAD_LENGTH. uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 8640; uint256 internal constant EXPECTED_VK_LENGTH = 8641; - uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0xaa09cafce41491f57d16b422d0224fa9090c5313065c1edccaf639ecd603e0fa; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x055be1a34918fad6a0d2545bab0bac0a28bbc5c616f3a9d284639391b7b99b3e; bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); // Solidity ABI calldata cursors. The generated verifier accepts exactly @@ -55,8 +94,8 @@ contract Halo2Verifier { uint256 internal constant INSTANCE_CPTR = 0x15c4; // First general-purpose memory words reserved by the generated verifier. // RETURN_MPTR is a single word set to 1 on success. - uint256 internal constant TRANSCRIPT_MPTR = 0x80; - uint256 internal constant RETURN_MPTR = 0x80; + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; // ---------------------------------------------------------------------- // Verifying-key memory map. The VK header lives at VK_MPTR, followed @@ -64,84 +103,87 @@ contract Halo2Verifier { // runtime comes the challenge slots (challenge_mptr..) and the // per-stage scratch (theta_mptr..). // ---------------------------------------------------------------------- - uint256 internal constant VK_MPTR = 0x1de0; - uint256 internal constant VK_DIGEST_MPTR = 0x1de0; - uint256 internal constant NUM_INSTANCES_MPTR = 0x1e00; - uint256 internal constant K_MPTR = 0x1e20; - uint256 internal constant N_INV_MPTR = 0x1e40; - uint256 internal constant OMEGA_MPTR = 0x1e60; - uint256 internal constant OMEGA_INV_MPTR = 0x1e80; - uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x1ea0; - uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x1ec0; - uint256 internal constant ACC_OFFSET_MPTR = 0x1ee0; - uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x1f00; - uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x1f20; - uint256 internal constant G1_BASE_MPTR = 0x1f40; - uint256 internal constant G2_BASE_MPTR = 0x1fc0; - uint256 internal constant NEG_S_G2_BASE_MPTR = 0x20c0; - - uint256 internal constant CHALLENGE_MPTR = 0x3fa0; + uint256 internal constant VK_MPTR = 0x2d60; + uint256 internal constant VK_DIGEST_MPTR = 0x2d60; + uint256 internal constant NUM_INSTANCES_MPTR = 0x2d80; + uint256 internal constant K_MPTR = 0x2da0; + uint256 internal constant N_INV_MPTR = 0x2dc0; + uint256 internal constant OMEGA_MPTR = 0x2de0; + uint256 internal constant OMEGA_INV_MPTR = 0x2e00; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x2e20; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x2e40; + uint256 internal constant ACC_OFFSET_MPTR = 0x2e60; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2e80; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x2ea0; + uint256 internal constant G1_BASE_MPTR = 0x2ec0; + uint256 internal constant G2_BASE_MPTR = 0x2f40; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x3040; + + uint256 internal constant CHALLENGE_MPTR = 0x4f20; // Challenge layout. Squeeze order in midnight-proofs: // user_phase challenges (variable count) // theta -> beta, gamma -> trash_challenge -> y -> x -> // x1, x2 -> x3 -> x4 - uint256 internal constant THETA_MPTR = 0x3fa0; - uint256 internal constant BETA_MPTR = 0x3fc0; - uint256 internal constant GAMMA_MPTR = 0x3fe0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x4000; - uint256 internal constant Y_MPTR = 0x4020; - uint256 internal constant X_MPTR = 0x4040; - uint256 internal constant X1_MPTR = 0x4060; - uint256 internal constant X2_MPTR = 0x4080; - uint256 internal constant X3_MPTR = 0x40a0; - uint256 internal constant X4_MPTR = 0x40c0; + uint256 internal constant THETA_MPTR = 0x4f20; + uint256 internal constant BETA_MPTR = 0x4f40; + uint256 internal constant GAMMA_MPTR = 0x4f60; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x4f80; + uint256 internal constant Y_MPTR = 0x4fa0; + uint256 internal constant X_MPTR = 0x4fc0; + uint256 internal constant X1_MPTR = 0x4fe0; + uint256 internal constant X2_MPTR = 0x5000; + uint256 internal constant X3_MPTR = 0x5020; + uint256 internal constant X4_MPTR = 0x5040; // Batch-open commitments live in 4-word EIP-2537 padded slots. - uint256 internal constant F_COM_MPTR = 0x40e0; - uint256 internal constant PI_MPTR = 0x4160; + uint256 internal constant F_COM_MPTR = 0x5060; + uint256 internal constant PI_MPTR = 0x50e0; // Accumulator (KZG IVC). - uint256 internal constant ACC_LHS_MPTR = 0x41e0; - uint256 internal constant ACC_RHS_MPTR = 0x4260; + uint256 internal constant ACC_LHS_MPTR = 0x5160; + uint256 internal constant ACC_RHS_MPTR = 0x51e0; // Lagrange / linearization scratch. - uint256 internal constant X_N_MPTR = 0x42e0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x4300; - uint256 internal constant L_LAST_MPTR = 0x4320; - uint256 internal constant L_BLIND_MPTR = 0x4340; - uint256 internal constant L_0_MPTR = 0x4360; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x4380; + uint256 internal constant X_N_MPTR = 0x5260; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x5280; + uint256 internal constant L_LAST_MPTR = 0x52a0; + uint256 internal constant L_BLIND_MPTR = 0x52c0; + uint256 internal constant L_0_MPTR = 0x52e0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x5300; // Legacy name: this is not h(x). It stores the expected opening // scalar for the linearized commitment, i.e. the negated y-batched // identity numerator reconstructed from the alleged evals at x. - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x43a0; - uint256 internal constant QUOTIENT_MPTR = 0x43c0; // 4 words - uint256 internal constant F_EVAL_MPTR = 0x4460; - uint256 internal constant V_MPTR = 0x4480; - uint256 internal constant FINAL_COM_MPTR = 0x44a0; // 4 words - uint256 internal constant PAIRING_LHS_MPTR = 0x4520; // 4 words - uint256 internal constant PAIRING_RHS_MPTR = 0x45a0; // 4 words + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x5320; + uint256 internal constant QUOTIENT_MPTR = 0x5340; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x53e0; + uint256 internal constant V_MPTR = 0x5400; + uint256 internal constant FINAL_COM_MPTR = 0x5420; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x54a0; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x5520; // 4 words // Multi-prepare scratch (sized at codegen time). - uint256 internal constant ROT_POINTS_MPTR = 0x4620; - uint256 internal constant X1_POWERS_MPTR = 0x49a0; + uint256 internal constant ROT_POINTS_MPTR = 0x55a0; + uint256 internal constant X1_POWERS_MPTR = 0x5920; // Q_COM materialization is currently fused into the final MSM scratch, // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero // reserved capacity until a future emitter starts writing Q_COM_MPTR. - uint256 internal constant Q_COM_MPTR = 0x51c0; - uint256 internal constant Q_EVAL_SET_MPTR = 0x51c0; + uint256 internal constant Q_COM_MPTR = 0x6140; + uint256 internal constant Q_EVAL_SET_MPTR = 0x6140; // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals // block of the proof; we keep it as a memory slot for symmetry. - uint256 internal constant Q_EVAL_CPTR_MPTR = 0x58c0; + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x6840; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. - uint256 internal constant G1_IDENTITY_MPTR = 0x59c0; + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x6940; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain // Solidity proof shim rewrites proof scalars into canonical BE words, @@ -149,11 +191,15 @@ contract Halo2Verifier { // side `evaluations` loop range-checks and spills that value here so // downstream eval references (gate evaluator + PCS q_eval Horner) // become 3-gas `mload(...)` instead of calldata reads. - uint256 internal constant REVERSED_EVALS_MPTR = 0x5b20; - uint256 internal constant SELECTOR_ACC_MPTR = 0x6ee0; - uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; - uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x6ee0; - uint256 internal constant TRACE_U256_MPTR = 0x9a00; + uint256 internal constant REVERSED_EVALS_MPTR = 0x6aa0; + uint256 internal constant SELECTOR_ACC_MPTR = 0x7e60; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x7e60; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0x8060; + uint256 internal constant TRACE_U256_MPTR = 0xa980; // ---------------------------------------------------------------------- // Per-category bases for EIP-2537 padded G1 commitments. The proof @@ -170,13 +216,63 @@ contract Halo2Verifier { // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans // ---------------------------------------------------------------------- - uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x63e0; - uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x67e0; - uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x68e0; - uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x6a60; - uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x6b60; - uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x6c60; - uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x6ce0; + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x7360; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x7760; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x7860; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x79e0; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x7ae0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x7be0; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x7c60; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 454608; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x360d2263de36925682a544990692d3d7e5c2f7beba81756f9a349f8544ea4123; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. @@ -195,10 +291,19 @@ contract Halo2Verifier { /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + // Scratch is reused for every runtime-prerequisite probe. - let scratch := 0x80 + let scratch := 0x1000 // MCOPY must be available because the verifier uses it for // proof-time point/scratch staging. Execute the opcode here so a @@ -216,23 +321,144 @@ contract Halo2Verifier { // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. - let msm_scratch := 0x6ee0 + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0x7e60 for { let off := 0 } lt(off, 0x2940) { off := add(off, 0x20) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x2940, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x2940, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -242,7 +468,7 @@ contract Halo2Verifier { // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } @@ -271,17 +497,33 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. @@ -298,7 +540,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } // Non-embedded renders pin the VK by address and codehash. The Yul @@ -306,24 +551,48 @@ contract Halo2Verifier { // INVALID-prefixed payload into VK_MPTR. address vk = AUTHORIZED_VK; assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== - // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of // a fixed post-VK address that can collide with live PCS scratch // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } - let p := 0x1ce0 + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x2c60 // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] mstore(add(p, 0x00), 0x20) // base len @@ -332,8 +601,8 @@ contract Halo2Verifier { mstore(add(p, 0x60), x) mstore(add(p, 0x80), sub(FR_MODULUS, 2)) mstore(add(p, 0xa0), FR_MODULUS) - if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -393,16 +662,16 @@ contract Halo2Verifier { let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -458,6 +727,13 @@ contract Halo2Verifier { // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -470,7 +746,7 @@ contract Halo2Verifier { mstore(add(single_scratch, 0x60), x) mstore(add(single_scratch, 0x80), sub(r, 2)) mstore(add(single_scratch, 0xa0), r) - ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) if ret { mstore(mptr_start, mload(single_scratch)) } leave @@ -478,16 +754,34 @@ contract Halo2Verifier { // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -502,8 +796,14 @@ contract Halo2Verifier { mstore(add(gp_mptr, 0x60), gp) mstore(add(gp_mptr, 0x80), sub(r, 2)) mstore(add(gp_mptr, 0xa0), r) - ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -528,22 +828,31 @@ contract Halo2Verifier { // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to // be a 4-step mstore chain for each G1 (~60 gas) and an // 8-iter mstore loop for each G2 (~240 gas). Net saving // here is ~500 gas per ec_pairing call. - let scratch := 0x0300 + let scratch := 0x1240 mcopy(scratch, lhs_mptr, 0x80) mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } @@ -574,7 +883,13 @@ contract Halo2Verifier { // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -742,6 +1057,14 @@ contract Halo2Verifier { // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -802,7 +1125,7 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -819,7 +1142,7 @@ contract Halo2Verifier { success := and(success, eq(mload(ACC_OFFSET_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 0)) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -843,7 +1166,7 @@ contract Halo2Verifier { ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } // =============================================================== @@ -913,7 +1236,7 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } // =============================================================== @@ -1096,7 +1419,7 @@ contract Halo2Verifier { // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, 0x20) @@ -1149,7 +1472,7 @@ contract Halo2Verifier { {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) proof_cptr := add(proof_cptr, 0x20) } @@ -1175,11 +1498,11 @@ contract Halo2Verifier { // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1198,8 +1521,10 @@ contract Halo2Verifier { // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, 0x0160) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1209,11 +1534,11 @@ contract Halo2Verifier { } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1224,9 +1549,9 @@ contract Halo2Verifier { // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, 0x0140) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0140) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -1249,8 +1574,8 @@ contract Halo2Verifier { // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, 0x0140)) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0140)) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -1260,9 +1585,19 @@ contract Halo2Verifier { mstore(INSTANCE_EVAL_MPTR, instance_eval) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } - // Optional quotient helper functions. Each one is rendered only + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr // helpers and share the same FR_MODULUS as the surrounding @@ -1282,7 +1617,8 @@ contract Halo2Verifier { let q_r := FR_MODULUS let x2 := mulmod(x, x, q_r) z := mulmod(x, mulmod(x2, x2, q_r), q_r) - } // =============================================================== + } + // =============================================================== // Batched identity numerator / linearization target. // // This block does not evaluate the quotient polynomial h(x), and @@ -1378,15 +1714,15 @@ contract Halo2Verifier { // q_const_mptr points to Fr constants used by the VM. // q_program_mptr points to the bytecode stream. // Constants are stored as consecutive 32-byte Fr words. - let q_const_mptr := 0x21c0 + let q_const_mptr := 0x3140 // Program bytes are also stored in the VK payload, packed into // 32-byte words by PackedProgramCodec. - let q_program_mptr := 0x27c0 + let q_program_mptr := 0x3740 // Running Horner accumulator for fully evaluated identities. // After all identities, this is nu_y(x) for the `None` // identity group. // Initialize A = 0 before scanning the identity stream. - mstore(0x70c0, 0) + mstore(0x8040, 0) // Simple selectors are grouped into separate linearization // buckets. They start at zero for every proof. // q_sel_zero_off walks selector bucket byte offsets. @@ -1401,12 +1737,19 @@ contract Halo2Verifier { { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0x8080, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, 42) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) // Store y^i at selector_power_mptr + 32*i. - mstore(add(0x7100, shl(5, q_y_power_i)), q_y_power) + mstore(add(0x8080, shl(5, q_y_power_i)), q_y_power) } } @@ -1415,102 +1758,102 @@ contract Halo2Verifier { // VM/native identities, so they occupy the same y-batch order. { let var0 := 0x1 - let f_3 := mload(0x5f60) - let f_4 := mload(0x5e60) - let a_0 := mload(0x5b40) + let f_3 := mload(0x6ee0) + let f_4 := mload(0x6de0) + let a_0 := mload(0x6ac0) let var1 := mulmod(f_4, a_0, r) let var2 := addmod(f_3, var1, r) - let f_5 := mload(0x5e80) - let a_1 := mload(0x5b60) + let f_5 := mload(0x6e00) + let a_1 := mload(0x6ae0) let var3 := mulmod(f_5, a_1, r) let var4 := addmod(var2, var3, r) - let f_6 := mload(0x5ea0) - let a_2 := mload(0x5b80) + let f_6 := mload(0x6e20) + let a_2 := mload(0x6b00) let var5 := mulmod(f_6, a_2, r) let var6 := addmod(var4, var5, r) - let f_7 := mload(0x5ec0) - let a_3 := mload(0x5ba0) + let f_7 := mload(0x6e40) + let a_3 := mload(0x6b20) let var7 := mulmod(f_7, a_3, r) let var8 := addmod(var6, var7, r) - let f_8 := mload(0x5ee0) - let a_4 := mload(0x5bc0) + let f_8 := mload(0x6e60) + let a_4 := mload(0x6b40) let var9 := mulmod(f_8, a_4, r) let var10 := addmod(var8, var9, r) - let f_0 := mload(0x5f00) - let a_0_next_1 := mload(0x5be0) + let f_0 := mload(0x6e80) + let a_0_next_1 := mload(0x6b60) let var11 := mulmod(f_0, a_0_next_1, r) let var12 := addmod(var10, var11, r) - let f_1 := mload(0x5f20) + let f_1 := mload(0x6ea0) let var13 := mulmod(f_1, a_0, r) let var14 := mulmod(var13, a_1, r) let var15 := addmod(var12, var14, r) - let f_2 := mload(0x5f40) + let f_2 := mload(0x6ec0) let var16 := mulmod(f_2, a_0, r) let var17 := mulmod(var16, a_2, r) let var18 := addmod(var15, var17, r) let var19 := mulmod(var0, var18, r) - mstore(0x7640, var19) + mstore(0x85c0, var19) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } { let var0 := 0x1 - let a_1 := mload(0x5b60) - let a_2 := mload(0x5b80) + let a_1 := mload(0x6ae0) + let a_2 := mload(0x6b00) let var1 := addmod(a_1, a_2, r) - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var2 := addmod(0, sub(r, a_3), r) let var3 := addmod(var1, var2, r) - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var4 := addmod(0, sub(r, a_4), r) let var5 := addmod(var3, var4, r) let var6 := mulmod(var0, var5, r) - mstore(0x7640, var6) + mstore(0x85c0, var6) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } { let var0 := 0x1 - let a_0 := mload(0x5b40) - let f_4 := mload(0x5e60) + let a_0 := mload(0x6ac0) + let f_4 := mload(0x6de0) let var1 := addmod(a_0, f_4, r) - let a_0_next_1 := mload(0x5be0) + let a_0_next_1 := mload(0x6b60) let var2 := addmod(0, sub(r, a_0_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x7640, var4) + mstore(0x85c0, var4) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } { let var0 := 0x1 - let a_1 := mload(0x5b60) - let f_5 := mload(0x5e80) + let a_1 := mload(0x6ae0) + let f_5 := mload(0x6e00) let var1 := addmod(a_1, f_5, r) - let a_1_next_1 := mload(0x5c00) + let a_1_next_1 := mload(0x6b80) let var2 := addmod(0, sub(r, a_1_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x7640, var4) + mstore(0x85c0, var4) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x7100, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x8080, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } // VM registers: @@ -1530,24 +1873,30 @@ contract Halo2Verifier { // q_end is an exclusive byte pointer for the VM loop. let q_end := add(q_program_mptr, 0x025d) // q_sp starts at the first free stack word. - let q_sp := 0x7640 + let q_sp := 0x85c0 // q_top is meaningless until q_has_top is set. let q_top := 0 // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x09 push_const_u8 + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x13 add_mul_const_u8_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity + // 0x1c lin7 + // 0x21 modarith7 // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -1571,6 +1920,7 @@ contract Halo2Verifier { // 64 KiB when this compact form is emitted. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } if q_has_top { mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) @@ -1582,6 +1932,7 @@ contract Halo2Verifier { case 0x06 { // The safety validator guarantees a spilled operand // exists before ADD. q_top is the right operand. + if eq(q_sp, 0x85c0) { q_program_fail() } q_sp := sub(q_sp, 0x20) q_top := addmod(mload(q_sp), q_top, r) } @@ -1618,6 +1969,7 @@ contract Halo2Verifier { // already range-checked Fr scalar in verifier memory. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_top := addmod(q_top, mload(q_ptr), r) } // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. @@ -1625,6 +1977,7 @@ contract Halo2Verifier { // In-place multiply by a planned memory word. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_top := mulmod(q_top, mload(q_ptr), r) } // VM 0x13 ADD_MUL_CONST_U8_MEM_U16: fused q_top += mem * const. @@ -1634,6 +1987,7 @@ contract Halo2Verifier { let q_ptr := shr(240, q_word) let qconst := byte(2, q_word) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_top := addmod( q_top, mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), @@ -1678,6 +2032,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1715,6 +2070,7 @@ contract Halo2Verifier { // whole identity is gated by mload(q_cond_ptr). q_cond_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_cond_ptr, 0x2d60), 0x45e0) { q_program_fail() } } let q_acc := 0 @@ -1749,6 +2105,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1761,12 +2118,14 @@ contract Halo2Verifier { for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { let q_lhs := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_lhs, 0x2d60), 0x45e0) { q_program_fail() } let q_lhs_value := mload(q_lhs) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { let q_word := mload(q_pc) let qconst := byte(0, q_word) let q_rhs := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_rhs, 0x2d60), 0x45e0) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -1786,6 +2145,8 @@ contract Halo2Verifier { let q_lhs_base := shr(240, q_pair_word) let q_rhs_base := and(shr(224, q_pair_word), 0xffff) q_pc := add(q_pc, 0x04) + if gt(sub(q_lhs_base, 0x2d60), 0x4520) { q_program_fail() } + if gt(sub(q_rhs_base, 0x2d60), 0x4520) { q_program_fail() } let q_coeff_pc := q_pc q_pc := add(q_pc, 13) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { @@ -1811,6 +2172,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2d60), 0x45e0) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1825,6 +2187,8 @@ contract Halo2Verifier { let q_lhs := and(shr(232, q_word), 0xffff) let q_rhs := and(shr(216, q_word), 0xffff) q_pc := add(q_pc, 5) + if gt(sub(q_lhs, 0x2d60), 0x45e0) { q_program_fail() } + if gt(sub(q_rhs, 0x2d60), 0x45e0) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -1861,70 +2225,70 @@ contract Halo2Verifier { // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := 0x7640 + q_sp := 0x85c0 // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. { let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 - let q_perm_vals := 0x7640 - let q_perm_sigmas := 0x7760 - let q_perm_z_cur := 0x7880 - let q_perm_z_next := 0x78e0 - let q_perm_z_last := 0x7940 - let q_perm_delta_base_ptr := 0x7980 + let q_perm_vals := 0x85c0 + let q_perm_sigmas := 0x86e0 + let q_perm_z_cur := 0x8800 + let q_perm_z_next := 0x8860 + let q_perm_z_last := 0x88c0 + let q_perm_delta_base_ptr := 0x8900 let q_perm_num_cols := 9 let q_perm_num_sets := 3 let q_perm_chunk_len := 3 let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 - mstore(add(q_perm_vals, 0x0), mload(0x5e40)) + mstore(add(q_perm_vals, 0x0), mload(0x6dc0)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x5b40, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x6ac0, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0xc0), mload(0x5b20)) + mstore(add(q_perm_vals, 0xc0), mload(0x6aa0)) mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) - mstore(add(q_perm_vals, 0x100), mload(0x5c40)) + mstore(add(q_perm_vals, 0x100), mload(0x6bc0)) { for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 9) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off - mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x60a0, q_perm_sigma_load_src_off))) + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x7020, q_perm_sigma_load_src_off))) } } { for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 3) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) - mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x61c0, q_perm_z_cur_load_src_off))) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x7140, q_perm_z_cur_load_src_off))) } } { for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 3) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) - mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x61e0, q_perm_z_next_load_src_off))) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x7160, q_perm_z_next_load_src_off))) } } - mstore(add(q_perm_z_last, 0x0), mload(0x6200)) - mstore(add(q_perm_z_last, 0x20), mload(0x6260)) + mstore(add(q_perm_z_last, 0x0), mload(0x7180)) + mstore(add(q_perm_z_last, 0x20), mload(0x71e0)) let q_perm_eval := 0 q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_perm_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_perm_eval, r)) let q_perm_zn := mload(add(q_perm_z_cur, 0x40)) q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_perm_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_perm_eval, r)) for { let q_perm_i := 1 } lt(q_perm_i, 3) { q_perm_i := add(q_perm_i, 1) } { let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_perm_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_perm_eval, r)) } mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) for { let q_perm_set := 0 } lt(q_perm_set, 3) { q_perm_set := add(q_perm_set, 1) } { @@ -1943,8 +2307,8 @@ contract Halo2Verifier { q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) } q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_perm_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_perm_eval, r)) mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) } } @@ -1965,13 +2329,13 @@ contract Halo2Verifier { // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := 0x7640 + q_sp := 0x85c0 // Generated LogUp code follows the same y-batch order // as the Rust identity stream. { - let q_lookup_f := 0x7640 - let q_lookup_prefix := 0x7680 - let q_lookup_suffix := 0x76c0 + let q_lookup_f := 0x85c0 + let q_lookup_prefix := 0x8600 + let q_lookup_suffix := 0x8640 let q_lookup_l0 := mload(L_0_MPTR) let q_lookup_llast := mload(L_LAST_MPTR) let q_lookup_lblind := mload(L_BLIND_MPTR) @@ -1981,54 +2345,54 @@ contract Halo2Verifier { let q_lookup_theta := mload(THETA_MPTR) { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x6300), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x7280), r) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } { - let f_10 := mload(0x5f80) + let f_10 := mload(0x6f00) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_1, r) - let q_lookup_eval := addmod(mulmod(mload(0x62e0), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x7260), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x62e0) - let f_19 := mload(0x6040) - let f_11 := mload(0x5fa0) + let q_lookup_sum_h := mload(0x7260) + let f_19 := mload(0x6fc0) + let f_11 := mload(0x6f20) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) - let f_12 := mload(0x5fc0) + let f_12 := mload(0x6f40) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) let q_lookup_s_sum_h := mulmod(f_19, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x6320), sub(r, addmod(mload(0x6300), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x72a0), sub(r, addmod(mload(0x7280), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x62c0), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x7240), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } } { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x6380), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x7300), r) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } { - let f_0 := mload(0x5f00) + let f_0 := mload(0x6e80) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_0, r) - let a_6 := mload(0x5c60) + let a_6 := mload(0x6be0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_6, r) - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_0, r) mstore(add(q_lookup_f, 0x0), addmod(var2, q_lookup_beta, r)) - let f_1 := mload(0x5f20) + let f_1 := mload(0x6ea0) let var3 := addmod(mulmod(0, q_lookup_theta, r), f_1, r) - let a_7 := mload(0x5c80) + let a_7 := mload(0x6c00) let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_7, r) - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_1, r) mstore(add(q_lookup_f, 0x20), addmod(var5, q_lookup_beta, r)) let q_lookup_product := 1 @@ -2049,26 +2413,26 @@ contract Halo2Verifier { for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 2) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) } - let q_lookup_eval := addmod(mulmod(mload(0x6360), q_lookup_product, r), sub(r, q_lookup_sum), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x72e0), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x6360) - let f_20 := mload(0x6060) - let f_13 := mload(0x5fe0) + let q_lookup_sum_h := mload(0x72e0) + let f_20 := mload(0x6fe0) + let f_13 := mload(0x6f60) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_13, r) - let f_14 := mload(0x6000) + let f_14 := mload(0x6f80) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_14, r) - let f_15 := mload(0x6020) + let f_15 := mload(0x6fa0) let var2 := addmod(mulmod(var1, q_lookup_theta, r), f_15, r) let q_lookup_s_sum_h := mulmod(f_20, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x63a0), sub(r, addmod(mload(0x6380), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x7320), sub(r, addmod(mload(0x7300), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var2, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x6340), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x72c0), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_lookup_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_lookup_eval, r)) } } } @@ -2089,24 +2453,24 @@ contract Halo2Verifier { // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := 0x7640 + q_sp := 0x85c0 // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx case 0 { { let var0 := 0x1 let var1 := 0x1000000000000000 - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var2 := mulmod(var1, a_4, r) let var3 := 0x10000000000 - let a_3_prev_1 := mload(0x5ca0) + let a_3_prev_1 := mload(0x6c20) let var4 := mulmod(var3, a_3_prev_1, r) let var5 := addmod(var2, var4, r) let var6 := 0x400000 - let a_4_prev_1 := mload(0x5cc0) + let a_4_prev_1 := mload(0x6c40) let var7 := mulmod(var6, a_4_prev_1, r) let var8 := addmod(var5, var7, r) - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var9 := addmod(var8, a_3, r) let var10 := 0x40000000000 let var11 := mulmod(var10, a_3, r) @@ -2128,54 +2492,54 @@ contract Halo2Verifier { let var27 := addmod(var24, var26, r) let var28 := addmod(var27, a_3_prev_1, r) let var29 := addmod(var19, var28, r) - let a_0_prev_1 := mload(0x5ce0) + let a_0_prev_1 := mload(0x6c60) let var30 := mulmod(var10, a_0_prev_1, r) - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var31 := mulmod(var25, a_0, r) let var32 := addmod(var30, var31, r) - let a_0_next_1 := mload(0x5be0) + let a_0_next_1 := mload(0x6b60) let var33 := addmod(var32, a_0_next_1, r) let var34 := 0x2 - let a_1_prev_1 := mload(0x5d00) + let a_1_prev_1 := mload(0x6c80) let var35 := mulmod(var10, a_1_prev_1, r) - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var36 := mulmod(var25, a_1, r) let var37 := addmod(var35, var36, r) - let a_1_next_1 := mload(0x5c00) + let a_1_next_1 := mload(0x6b80) let var38 := addmod(var37, a_1_next_1, r) let var39 := mulmod(var34, var38, r) let var40 := addmod(var33, var39, r) let var41 := addmod(0, sub(r, var40), r) let var42 := addmod(var29, var41, r) let var43 := mulmod(var0, var42, r) - mstore(0x7640, var43) + mstore(0x85c0, var43) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xa0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } } case 1 { { let var0 := 0x1 let var1 := 0x10000000000000 - let a_3_next_1 := mload(0x5d20) + let a_3_next_1 := mload(0x6ca0) let var2 := mulmod(var1, a_3_next_1, r) let var3 := 0x4000000000 - let a_3_prev_1 := mload(0x5ca0) + let a_3_prev_1 := mload(0x6c20) let var4 := mulmod(var3, a_3_prev_1, r) let var5 := addmod(var2, var4, r) let var6 := 0x4000 - let a_4_prev_1 := mload(0x5cc0) + let a_4_prev_1 := mload(0x6c40) let var7 := mulmod(var6, a_4_prev_1, r) let var8 := addmod(var5, var7, r) let var9 := 0x400 - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var10 := mulmod(var9, a_3, r) let var11 := addmod(var8, var10, r) - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var12 := addmod(var11, a_4, r) let var13 := 0x40000000000000 let var14 := mulmod(var13, a_4, r) @@ -2202,115 +2566,115 @@ contract Halo2Verifier { let var35 := addmod(var33, var34, r) let var36 := addmod(var35, a_3_prev_1, r) let var37 := addmod(var25, var36, r) - let a_0_prev_1 := mload(0x5ce0) + let a_0_prev_1 := mload(0x6c60) let var38 := mulmod(var15, a_0_prev_1, r) let var39 := 0x100000 - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var40 := mulmod(var39, a_0, r) let var41 := addmod(var38, var40, r) - let a_0_next_1 := mload(0x5be0) + let a_0_next_1 := mload(0x6b60) let var42 := addmod(var41, a_0_next_1, r) let var43 := 0x2 - let a_1_prev_1 := mload(0x5d00) + let a_1_prev_1 := mload(0x6c80) let var44 := mulmod(var15, a_1_prev_1, r) - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var45 := mulmod(var39, a_1, r) let var46 := addmod(var44, var45, r) - let a_1_next_1 := mload(0x5c00) + let a_1_next_1 := mload(0x6b80) let var47 := addmod(var46, a_1_next_1, r) let var48 := mulmod(var43, var47, r) let var49 := addmod(var42, var48, r) let var50 := addmod(0, sub(r, var49), r) let var51 := addmod(var37, var50, r) let var52 := mulmod(var0, var51, r) - mstore(0x7640, var52) + mstore(0x85c0, var52) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xc0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } } case 2 { { let var0 := 0x1 - let f_0 := mload(0x5f00) - let a_0_next_1 := mload(0x5be0) + let f_0 := mload(0x6e80) + let a_0_next_1 := mload(0x6b60) let var1 := addmod(0, sub(r, a_0_next_1), r) let var2 := addmod(f_0, var1, r) let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_2 := mload(0x5b80) + let a_2 := mload(0x6b00) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x5c40) + let a_5 := mload(0x6bc0) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x7640, var18) + mstore(0x85c0, var18) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x1c0) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x7100, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x8080, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } } case 3 { { let var0 := 0x1 - let f_1 := mload(0x5f20) - let a_1_next_1 := mload(0x5c00) + let f_1 := mload(0x6ea0) + let a_1_next_1 := mload(0x6b80) let var1 := addmod(0, sub(r, a_1_next_1), r) let var2 := addmod(f_1, var1, r) let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f - let a_2 := mload(0x5b80) + let a_2 := mload(0x6b00) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x5c40) + let a_5 := mload(0x6bc0) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x7640, var18) + mstore(0x85c0, var18) } - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x1c0) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x7100, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7640), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x8080, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x85c0), r)) } } - default { revert(0, 0) } + default { q_program_fail() } } // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. case 0x0b { @@ -2322,6 +2686,11 @@ contract Halo2Verifier { q_pc := add(q_pc, 3) let q_sel_idx := shr(16, q_selector_payload) let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 15)) { q_program_fail() } + if gt(q_sel_gap, 0x29) { q_program_fail() } let q_eval := q_top q_has_top := 0 // Simple-selector identity: keep the same y-batch @@ -2331,28 +2700,34 @@ contract Halo2Verifier { // The global fully-evaluated accumulator is still // multiplied by y so later main identities land at the // same y powers as Rust's reverse fold. - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) let q_sel_acc := mload(q_target_ptr) if q_sel_gap { // Selector buckets are sparse in the global // identity stream. Precomputed y^gap advances only // this selector's local accumulator. - q_sel_acc := mulmod(q_sel_acc, mload(add(0x7100, shl(5, q_sel_gap))), r) + q_sel_acc := mulmod(q_sel_acc, mload(add(0x8080, shl(5, q_sel_gap))), r) } mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) } // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0x85c0)) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -2364,52 +2739,52 @@ contract Halo2Verifier { { let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) { - let f_0 := mload(0x5f00) - let a_0_next_1 := mload(0x5be0) + let f_0 := mload(0x6e80) + let a_0_next_1 := mload(0x6b60) let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 let var1 := mulmod(a_0_next_1, var0, r) let var2 := addmod(f_0, var1, r) let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 - let a_0 := mload(0x5b40) + let a_0 := mload(0x6ac0) let var4 := mulmod(var3, a_0, r) let var5 := addmod(var2, var4, r) let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f - let a_1 := mload(0x5b60) + let a_1 := mload(0x6ae0) let var7 := mulmod(var6, a_1, r) let var8 := addmod(var5, var7, r) let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 - let a_2 := mload(0x5b80) + let a_2 := mload(0x6b00) let var10 := q_pow5(a_2) let var11 := mulmod(var9, var10, r) let var12 := addmod(var8, var11, r) let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 - let a_3 := mload(0x5ba0) + let a_3 := mload(0x6b20) let var14 := q_pow5(a_3) let var15 := mulmod(var13, var14, r) let var16 := addmod(var12, var15, r) let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 - let a_4 := mload(0x5bc0) + let a_4 := mload(0x6b40) let var18 := q_pow5(a_4) let var19 := mulmod(var17, var18, r) let var20 := addmod(var16, var19, r) let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e - let a_5 := mload(0x5c40) + let a_5 := mload(0x6bc0) let var22 := q_pow5(a_5) let var23 := mulmod(var21, var22, r) let var24 := addmod(var20, var23, r) let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 - let a_6 := mload(0x5c60) + let a_6 := mload(0x6be0) let var26 := q_pow5(a_6) let var27 := mulmod(var25, var26, r) let var28 := addmod(var24, var27, r) let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_7 := mload(0x5c80) + let a_7 := mload(0x6c00) let var30 := q_pow5(a_7) let var31 := mulmod(var29, var30, r) let var32 := addmod(var28, var31, r) let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) - let f_1 := mload(0x5f20) - let a_1_next_1 := mload(0x5c00) + let f_1 := mload(0x6ea0) + let a_1_next_1 := mload(0x6b80) let var34 := mulmod(a_1_next_1, var0, r) let var35 := addmod(f_1, var34, r) let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d @@ -2437,7 +2812,7 @@ contract Halo2Verifier { let var58 := mulmod(var57, var30, r) let var59 := addmod(var56, var58, r) let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) - let f_2 := mload(0x5f40) + let f_2 := mload(0x6ec0) let var61 := mulmod(a_3, var0, r) let var62 := addmod(f_2, var61, r) let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 @@ -2450,7 +2825,7 @@ contract Halo2Verifier { let var70 := mulmod(var69, var10, r) let var71 := addmod(var68, var70, r) let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) - let f_3 := mload(0x5f60) + let f_3 := mload(0x6ee0) let var73 := mulmod(a_4, var0, r) let var74 := addmod(f_3, var73, r) let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 @@ -2465,7 +2840,7 @@ contract Halo2Verifier { let var84 := mulmod(var69, var14, r) let var85 := addmod(var83, var84, r) let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) - let f_4 := mload(0x5e60) + let f_4 := mload(0x6de0) let var87 := mulmod(a_5, var0, r) let var88 := addmod(f_4, var87, r) let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b @@ -2482,7 +2857,7 @@ contract Halo2Verifier { let var100 := mulmod(var69, var18, r) let var101 := addmod(var99, var100, r) let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) - let f_5 := mload(0x5e80) + let f_5 := mload(0x6e00) let var103 := mulmod(a_6, var0, r) let var104 := addmod(f_5, var103, r) let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 @@ -2501,7 +2876,7 @@ contract Halo2Verifier { let var118 := mulmod(var69, var22, r) let var119 := addmod(var117, var118, r) let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) - let f_6 := mload(0x5ea0) + let f_6 := mload(0x6e20) let var121 := mulmod(a_7, var0, r) let var122 := addmod(f_6, var121, r) let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a @@ -2522,8 +2897,8 @@ contract Halo2Verifier { let var138 := mulmod(var69, var26, r) let var139 := addmod(var137, var138, r) let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) - let f_7 := mload(0x5ec0) - let a_2_next_1 := mload(0x5c20) + let f_7 := mload(0x6e40) + let a_2_next_1 := mload(0x6ba0) let var141 := mulmod(a_2_next_1, var0, r) let var142 := addmod(f_7, var141, r) let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 @@ -2546,12 +2921,12 @@ contract Halo2Verifier { let var160 := mulmod(var69, var30, r) let var161 := addmod(var159, var160, r) let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) - let f_33 := mload(0x6080) + let f_33 := mload(0x7000) let q_trash_one_minus_selector := addmod(1, sub(r, f_33), r) - let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x63c0), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x7340), r) let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) - mstore(0x70c0, mulmod(mload(0x70c0), y, r)) - mstore(0x70c0, addmod(mload(0x70c0), q_trash_eval, r)) + mstore(0x8040, mulmod(mload(0x8040), y, r)) + mstore(0x8040, addmod(mload(0x8040), q_trash_eval, r)) } } // Finish selector buckets by applying the codegen-known tail @@ -2563,69 +2938,69 @@ contract Halo2Verifier { // selector commitment in the linearized MSM. { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0520)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0520)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0500)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0500)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x04a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x04a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0480)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0480)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0440)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0440)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0420)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0420)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0400)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0400)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x03e0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x03e0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x03c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x03c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x03a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x03a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0140) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0360)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0360)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0160) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0320)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0320)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0180) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x02a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x02a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x01a0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x0280)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x0280)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x01c0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7100, 0x01c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x8080, 0x01c0)), r)) } // Fully evaluated identities are the constant-polynomial side // of the linearization query. Rust subtracts that grouped // scalar into expected_eval, so Solidity stores -nu_y(x). - let linearization_expected_eval := addmod(0, sub(r, mload(0x70c0)), r) + let linearization_expected_eval := addmod(0, sub(r, mload(0x8040)), r) mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } @@ -2727,46 +3102,46 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[0]: 35 commitment(s) (rolled, m>=4) + // q_eval_set[0]: 35 evaluation term(s), 34 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x70c0, 0x5b20) - mstore(0x70e0, 0x62c0) - mstore(0x7100, 0x62e0) - mstore(0x7120, 0x6340) - mstore(0x7140, 0x6360) - mstore(0x7160, 0x63c0) - mstore(0x7180, 0x5e40) - mstore(0x71a0, 0x5e60) - mstore(0x71c0, 0x5e80) - mstore(0x71e0, 0x5ea0) - mstore(0x7200, 0x5ec0) - mstore(0x7220, 0x5ee0) - mstore(0x7240, 0x5f00) - mstore(0x7260, 0x5f20) - mstore(0x7280, 0x5f40) - mstore(0x72a0, 0x5f60) - mstore(0x72c0, 0x5f80) - mstore(0x72e0, 0x5fa0) - mstore(0x7300, 0x5fc0) - mstore(0x7320, 0x5fe0) - mstore(0x7340, 0x6000) - mstore(0x7360, 0x6020) - mstore(0x7380, 0x6040) - mstore(0x73a0, 0x6060) - mstore(0x73c0, 0x6080) - mstore(0x73e0, 0x60a0) - mstore(0x7400, 0x60c0) - mstore(0x7420, 0x60e0) - mstore(0x7440, 0x6100) - mstore(0x7460, 0x6120) - mstore(0x7480, 0x6140) - mstore(0x74a0, 0x6160) - mstore(0x74c0, 0x6180) - mstore(0x74e0, 0x61a0) - mstore(0x7500, QUOTIENT_EVAL_MPTR) - let q_eval_set_0 := mload(0x5b20) + mstore(0x8040, 0x6aa0) + mstore(0x8060, 0x7240) + mstore(0x8080, 0x7260) + mstore(0x80a0, 0x72c0) + mstore(0x80c0, 0x72e0) + mstore(0x80e0, 0x7340) + mstore(0x8100, 0x6dc0) + mstore(0x8120, 0x6de0) + mstore(0x8140, 0x6e00) + mstore(0x8160, 0x6e20) + mstore(0x8180, 0x6e40) + mstore(0x81a0, 0x6e60) + mstore(0x81c0, 0x6e80) + mstore(0x81e0, 0x6ea0) + mstore(0x8200, 0x6ec0) + mstore(0x8220, 0x6ee0) + mstore(0x8240, 0x6f00) + mstore(0x8260, 0x6f20) + mstore(0x8280, 0x6f40) + mstore(0x82a0, 0x6f60) + mstore(0x82c0, 0x6f80) + mstore(0x82e0, 0x6fa0) + mstore(0x8300, 0x6fc0) + mstore(0x8320, 0x6fe0) + mstore(0x8340, 0x7000) + mstore(0x8360, 0x7020) + mstore(0x8380, 0x7040) + mstore(0x83a0, 0x7060) + mstore(0x83c0, 0x7080) + mstore(0x83e0, 0x70a0) + mstore(0x8400, 0x70c0) + mstore(0x8420, 0x70e0) + mstore(0x8440, 0x7100) + mstore(0x8460, 0x7120) + mstore(0x8480, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x6aa0) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x70c0, 0x20) + let eval_p := add(0x8040, 0x20) for { let i := 1 } lt(i, 0x23) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2779,13 +3154,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[1]: 3 commitment(s) - let q_eval_set_0 := mload(0x6280) - let q_eval_set_1 := mload(0x62a0) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6300), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6320), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6380), mload(add(X1_POWERS_MPTR, 0x40)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x63a0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + // q_eval_set[1]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x7200) + let q_eval_set_1 := mload(0x7220) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x7280), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x72a0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x7300), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x7320), mload(add(X1_POWERS_MPTR, 0x40)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } @@ -2793,37 +3168,37 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[2]: 8 commitment(s) (rolled, m>=4) + // q_eval_set[2]: 8 evaluation term(s), 8 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x70c0, 0x5b40) - mstore(0x70e0, 0x5be0) - mstore(0x7100, 0x5ce0) - mstore(0x7120, 0x5b60) - mstore(0x7140, 0x5c00) - mstore(0x7160, 0x5d00) - mstore(0x7180, 0x5b80) - mstore(0x71a0, 0x5c20) - mstore(0x71c0, 0x5da0) - mstore(0x71e0, 0x5ba0) - mstore(0x7200, 0x5d20) - mstore(0x7220, 0x5ca0) - mstore(0x7240, 0x5bc0) - mstore(0x7260, 0x5d40) - mstore(0x7280, 0x5cc0) - mstore(0x72a0, 0x5c40) - mstore(0x72c0, 0x5e00) - mstore(0x72e0, 0x5de0) - mstore(0x7300, 0x5c60) - mstore(0x7320, 0x5d80) - mstore(0x7340, 0x5d60) - mstore(0x7360, 0x5c80) - mstore(0x7380, 0x5e20) - mstore(0x73a0, 0x5dc0) - let q_eval_set_0 := mload(0x5b40) - let q_eval_set_1 := mload(0x5be0) - let q_eval_set_2 := mload(0x5ce0) + mstore(0x8040, 0x6ac0) + mstore(0x8060, 0x6b60) + mstore(0x8080, 0x6c60) + mstore(0x80a0, 0x6ae0) + mstore(0x80c0, 0x6b80) + mstore(0x80e0, 0x6c80) + mstore(0x8100, 0x6b00) + mstore(0x8120, 0x6ba0) + mstore(0x8140, 0x6d20) + mstore(0x8160, 0x6b20) + mstore(0x8180, 0x6ca0) + mstore(0x81a0, 0x6c20) + mstore(0x81c0, 0x6b40) + mstore(0x81e0, 0x6cc0) + mstore(0x8200, 0x6c40) + mstore(0x8220, 0x6bc0) + mstore(0x8240, 0x6d80) + mstore(0x8260, 0x6d60) + mstore(0x8280, 0x6be0) + mstore(0x82a0, 0x6d00) + mstore(0x82c0, 0x6ce0) + mstore(0x82e0, 0x6c00) + mstore(0x8300, 0x6da0) + mstore(0x8320, 0x6d40) + let q_eval_set_0 := mload(0x6ac0) + let q_eval_set_1 := mload(0x6b60) + let q_eval_set_2 := mload(0x6c60) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x70c0, 0x60) + let eval_p := add(0x8040, 0x60) for { let i := 1 } lt(i, 0x8) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2840,13 +3215,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[3]: 2 commitment(s) - let q_eval_set_0 := mload(0x61c0) - let q_eval_set_1 := mload(0x61e0) - let q_eval_set_2 := mload(0x6200) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6220), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6240), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x6260), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + // q_eval_set[3]: 2 evaluation term(s), 2 commitment term(s) + let q_eval_set_0 := mload(0x7140) + let q_eval_set_1 := mload(0x7160) + let q_eval_set_2 := mload(0x7180) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x71a0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x71c0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x71e0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x100), q_eval_set_2) @@ -3014,145 +3389,146 @@ contract Halo2Verifier { v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_4, r), r) - mcopy(0x70c0, 0x67e0, 0x80) - mstore(0x7140, mload(add(X1_POWERS_MPTR, 0x20))) - mcopy(0x7160, 0x6a60, 0x80) - mstore(0x71e0, mload(add(X1_POWERS_MPTR, 0x40))) - mcopy(0x7200, 0x6860, 0x80) - mstore(0x7280, mload(add(X1_POWERS_MPTR, 0x60))) - mcopy(0x72a0, 0x6ae0, 0x80) - mstore(0x7320, mload(add(X1_POWERS_MPTR, 0x80))) - mcopy(0x7340, 0x6c60, 0x80) - mstore(0x73c0, mload(add(X1_POWERS_MPTR, 0xa0))) - mcopy(0x73e0, 0x2ea0, 0x80) - mstore(0x7460, mload(add(X1_POWERS_MPTR, 0xc0))) - mcopy(0x7480, 0x2c20, 0x80) - mstore(0x7500, mload(add(X1_POWERS_MPTR, 0xe0))) - mcopy(0x7520, 0x2ca0, 0x80) - mstore(0x75a0, mload(add(X1_POWERS_MPTR, 0x100))) - mcopy(0x75c0, 0x2d20, 0x80) - mstore(0x7640, mload(add(X1_POWERS_MPTR, 0x120))) - mcopy(0x7660, 0x2da0, 0x80) - mstore(0x76e0, mload(add(X1_POWERS_MPTR, 0x140))) - mcopy(0x7700, 0x2e20, 0x80) - mstore(0x7780, mload(add(X1_POWERS_MPTR, 0x160))) - mcopy(0x77a0, 0x2a20, 0x80) - mstore(0x7820, mload(add(X1_POWERS_MPTR, 0x180))) - mcopy(0x7840, 0x2aa0, 0x80) - mstore(0x78c0, mload(add(X1_POWERS_MPTR, 0x1a0))) - mcopy(0x78e0, 0x2b20, 0x80) - mstore(0x7960, mload(add(X1_POWERS_MPTR, 0x1c0))) - mcopy(0x7980, 0x2ba0, 0x80) - mstore(0x7a00, mload(add(X1_POWERS_MPTR, 0x1e0))) - mcopy(0x7a20, 0x2f20, 0x80) - mstore(0x7aa0, mload(add(X1_POWERS_MPTR, 0x200))) - mcopy(0x7ac0, 0x2fa0, 0x80) - mstore(0x7b40, mload(add(X1_POWERS_MPTR, 0x220))) - mcopy(0x7b60, 0x3020, 0x80) - mstore(0x7be0, mload(add(X1_POWERS_MPTR, 0x240))) - mcopy(0x7c00, 0x30a0, 0x80) - mstore(0x7c80, mload(add(X1_POWERS_MPTR, 0x260))) - mcopy(0x7ca0, 0x3120, 0x80) - mstore(0x7d20, mload(add(X1_POWERS_MPTR, 0x280))) - mcopy(0x7d40, 0x31a0, 0x80) - mstore(0x7dc0, mload(add(X1_POWERS_MPTR, 0x2a0))) - mcopy(0x7de0, 0x33a0, 0x80) - mstore(0x7e60, mload(add(X1_POWERS_MPTR, 0x2c0))) - mcopy(0x7e80, 0x3420, 0x80) - mstore(0x7f00, mload(add(X1_POWERS_MPTR, 0x2e0))) - mcopy(0x7f20, 0x3aa0, 0x80) - mstore(0x7fa0, mload(add(X1_POWERS_MPTR, 0x300))) - mcopy(0x7fc0, 0x3b20, 0x80) - mstore(0x8040, mload(add(X1_POWERS_MPTR, 0x320))) - mcopy(0x8060, 0x3ba0, 0x80) - mstore(0x80e0, mload(add(X1_POWERS_MPTR, 0x340))) - mcopy(0x8100, 0x3c20, 0x80) - mstore(0x8180, mload(add(X1_POWERS_MPTR, 0x360))) - mcopy(0x81a0, 0x3ca0, 0x80) - mstore(0x8220, mload(add(X1_POWERS_MPTR, 0x380))) - mcopy(0x8240, 0x3d20, 0x80) - mstore(0x82c0, mload(add(X1_POWERS_MPTR, 0x3a0))) - mcopy(0x82e0, 0x3da0, 0x80) - mstore(0x8360, mload(add(X1_POWERS_MPTR, 0x3c0))) - mcopy(0x8380, 0x3e20, 0x80) - mstore(0x8400, mload(add(X1_POWERS_MPTR, 0x3e0))) - mcopy(0x8420, 0x3ea0, 0x80) - mstore(0x84a0, mload(add(X1_POWERS_MPTR, 0x400))) - mcopy(0x84c0, 0x3f20, 0x80) - mstore(0x8540, mload(add(X1_POWERS_MPTR, 0x420))) + mcopy(0x8040, 0x7760, 0x80) + mstore(0x80c0, mload(add(X1_POWERS_MPTR, 0x20))) + mcopy(0x80e0, 0x79e0, 0x80) + mstore(0x8160, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0x8180, 0x77e0, 0x80) + mstore(0x8200, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0x8220, 0x7a60, 0x80) + mstore(0x82a0, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0x82c0, 0x7be0, 0x80) + mstore(0x8340, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0x8360, 0x3e20, 0x80) + mstore(0x83e0, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0x8400, 0x3ba0, 0x80) + mstore(0x8480, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0x84a0, 0x3c20, 0x80) + mstore(0x8520, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0x8540, 0x3ca0, 0x80) + mstore(0x85c0, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0x85e0, 0x3d20, 0x80) + mstore(0x8660, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0x8680, 0x3da0, 0x80) + mstore(0x8700, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0x8720, 0x39a0, 0x80) + mstore(0x87a0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0x87c0, 0x3a20, 0x80) + mstore(0x8840, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0x8860, 0x3aa0, 0x80) + mstore(0x88e0, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0x8900, 0x3b20, 0x80) + mstore(0x8980, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0x89a0, 0x3ea0, 0x80) + mstore(0x8a20, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0x8a40, 0x3f20, 0x80) + mstore(0x8ac0, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0x8ae0, 0x3fa0, 0x80) + mstore(0x8b60, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0x8b80, 0x4020, 0x80) + mstore(0x8c00, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0x8c20, 0x40a0, 0x80) + mstore(0x8ca0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0x8cc0, 0x4120, 0x80) + mstore(0x8d40, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0x8d60, 0x4320, 0x80) + mstore(0x8de0, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0x8e00, 0x43a0, 0x80) + mstore(0x8e80, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0x8ea0, 0x4a20, 0x80) + mstore(0x8f20, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0x8f40, 0x4aa0, 0x80) + mstore(0x8fc0, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0x8fe0, 0x4b20, 0x80) + mstore(0x9060, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0x9080, 0x4ba0, 0x80) + mstore(0x9100, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0x9120, 0x4c20, 0x80) + mstore(0x91a0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0x91c0, 0x4ca0, 0x80) + mstore(0x9240, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0x9260, 0x4d20, 0x80) + mstore(0x92e0, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0x9300, 0x4da0, 0x80) + mstore(0x9380, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0x93a0, 0x4e20, 0x80) + mstore(0x9420, mload(add(X1_POWERS_MPTR, 0x400))) + mcopy(0x9440, 0x4ea0, 0x80) + mstore(0x94c0, mload(add(X1_POWERS_MPTR, 0x420))) let lin_query_scalar_33 := mload(add(X1_POWERS_MPTR, 0x440)) let lin_cur_scalar_33 := mulmod(lin_query_scalar_33, lin_one_minus_x_n, r) - mcopy(0x8560, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) - mstore(0x85e0, lin_cur_scalar_33) + mcopy(0x94e0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0x9560, lin_cur_scalar_33) lin_cur_scalar_33 := mulmod(lin_cur_scalar_33, lin_x_split, r) - mcopy(0x8600, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) - mstore(0x8680, lin_cur_scalar_33) + mcopy(0x9580, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0x9600, lin_cur_scalar_33) lin_cur_scalar_33 := mulmod(lin_cur_scalar_33, lin_x_split, r) - mcopy(0x86a0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) - mstore(0x8720, lin_cur_scalar_33) + mcopy(0x9620, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0x96a0, lin_cur_scalar_33) lin_cur_scalar_33 := mulmod(lin_cur_scalar_33, lin_x_split, r) - mcopy(0x8740, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) - mstore(0x87c0, lin_cur_scalar_33) - mcopy(0x87e0, 0x3220, 0x80) - mstore(0x8860, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) - mcopy(0x8880, 0x32a0, 0x80) - mstore(0x8900, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) - mcopy(0x8920, 0x3320, 0x80) - mstore(0x89a0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) - mcopy(0x89c0, 0x34a0, 0x80) - mstore(0x8a40, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) - mcopy(0x8a60, 0x3520, 0x80) - mstore(0x8ae0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) - mcopy(0x8b00, 0x35a0, 0x80) - mstore(0x8b80, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) - mcopy(0x8ba0, 0x3620, 0x80) - mstore(0x8c20, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) - mcopy(0x8c40, 0x36a0, 0x80) - mstore(0x8cc0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) - mcopy(0x8ce0, 0x3720, 0x80) - mstore(0x8d60, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) - mcopy(0x8d80, 0x37a0, 0x80) - mstore(0x8e00, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) - mcopy(0x8e20, 0x3820, 0x80) - mstore(0x8ea0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x140)), r)) - mcopy(0x8ec0, 0x38a0, 0x80) - mstore(0x8f40, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x160)), r)) - mcopy(0x8f60, 0x3920, 0x80) - mstore(0x8fe0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x180)), r)) - mcopy(0x9000, 0x39a0, 0x80) - mstore(0x9080, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x1a0)), r)) - mcopy(0x90a0, 0x3a20, 0x80) - mstore(0x9120, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x1c0)), r)) - mcopy(0x9140, 0x69e0, 0x80) - mstore(0x91c0, x4_pow_1) - mcopy(0x91e0, 0x6b60, 0x80) - mstore(0x9260, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) - mcopy(0x9280, 0x6be0, 0x80) - mstore(0x9300, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) - mcopy(0x9320, 0x63e0, 0x80) - mstore(0x93a0, x4_pow_2) - mcopy(0x93c0, 0x6460, 0x80) - mstore(0x9440, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) - mcopy(0x9460, 0x64e0, 0x80) - mstore(0x94e0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) - mcopy(0x9500, 0x6560, 0x80) - mstore(0x9580, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_2, r)) - mcopy(0x95a0, 0x65e0, 0x80) - mstore(0x9620, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_2, r)) - mcopy(0x9640, 0x6660, 0x80) - mstore(0x96c0, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_2, r)) - mcopy(0x96e0, 0x66e0, 0x80) - mstore(0x9760, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_2, r)) - mcopy(0x9780, 0x6760, 0x80) - mstore(0x9800, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_2, r)) - mcopy(0x9820, 0x68e0, 0x80) - mstore(0x98a0, x4_pow_3) - mcopy(0x98c0, 0x6960, 0x80) - mstore(0x9940, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) - mcopy(0x9960, F_COM_MPTR, 0x80) - mstore(0x99e0, x4_pow_4) + mcopy(0x96c0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0x9740, lin_cur_scalar_33) + mcopy(0x9760, 0x41a0, 0x80) + mstore(0x97e0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0x9800, 0x4220, 0x80) + mstore(0x9880, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0x98a0, 0x42a0, 0x80) + mstore(0x9920, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0x9940, 0x4420, 0x80) + mstore(0x99c0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0x99e0, 0x44a0, 0x80) + mstore(0x9a60, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0x9a80, 0x4520, 0x80) + mstore(0x9b00, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0x9b20, 0x45a0, 0x80) + mstore(0x9ba0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0x9bc0, 0x4620, 0x80) + mstore(0x9c40, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0x9c60, 0x46a0, 0x80) + mstore(0x9ce0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0x9d00, 0x4720, 0x80) + mstore(0x9d80, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0x9da0, 0x47a0, 0x80) + mstore(0x9e20, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x140)), r)) + mcopy(0x9e40, 0x4820, 0x80) + mstore(0x9ec0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x160)), r)) + mcopy(0x9ee0, 0x48a0, 0x80) + mstore(0x9f60, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x180)), r)) + mcopy(0x9f80, 0x4920, 0x80) + mstore(0xa000, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x1a0)), r)) + mcopy(0xa020, 0x49a0, 0x80) + mstore(0xa0a0, mulmod(lin_query_scalar_33, mload(add(SELECTOR_ACC_MPTR, 0x1c0)), r)) + mcopy(0xa0c0, 0x7960, 0x80) + mstore(0xa140, x4_pow_1) + mcopy(0xa160, 0x7ae0, 0x80) + mstore(0xa1e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0xa200, 0x7b60, 0x80) + mstore(0xa280, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0xa2a0, 0x7360, 0x80) + mstore(0xa320, x4_pow_2) + mcopy(0xa340, 0x73e0, 0x80) + mstore(0xa3c0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0xa3e0, 0x7460, 0x80) + mstore(0xa460, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0xa480, 0x74e0, 0x80) + mstore(0xa500, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_2, r)) + mcopy(0xa520, 0x7560, 0x80) + mstore(0xa5a0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_2, r)) + mcopy(0xa5c0, 0x75e0, 0x80) + mstore(0xa640, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_2, r)) + mcopy(0xa660, 0x7660, 0x80) + mstore(0xa6e0, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_2, r)) + mcopy(0xa700, 0x76e0, 0x80) + mstore(0xa780, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_2, r)) + mcopy(0xa7a0, 0x7860, 0x80) + mstore(0xa820, x4_pow_3) + mcopy(0xa840, 0x78e0, 0x80) + mstore(0xa8c0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xa8e0, F_COM_MPTR, 0x80) + mstore(0xa960, x4_pow_4) if success { - success := staticcall(gas(), 0x0c, 0x70c0, 0x2940, FINAL_COM_MPTR, 0x80) + // exact EIP-2537 G1MSM cost for 66 pair(s) + success := staticcall(454608, 0x0c, 0x8040, 0x2940, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) @@ -3164,28 +3540,28 @@ contract Halo2Verifier { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) - mcopy(0x80, G1_BASE_MPTR, 0x80) - mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, FINAL_COM_MPTR, 0x80) + mcopy(0x1080, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, PI_MPTR, 0x80) - mstore(0x180, mload(X3_MPTR)) + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) if success { - success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) } } @@ -3216,13 +3592,19 @@ contract Halo2Verifier { // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) } diff --git a/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2VerifyingKey.sol b/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2VerifyingKey.sol index 17578c207..a4b803555 100644 --- a/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/target/hybrid-mt-fixture-dump/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. @@ -94,8 +97,8 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x03a0), 0x00000000000000000000000000000000006d57f79a18220d1e5ef04bd519e995) // neg_s_g2_y_c1_hi mstore(add(payload, 0x03c0), 0x9a9cc71553bb761b5422a6b6971b75c8d3695bfa07b861c4b1c958da426efc45) // neg_s_g2_y_c1_lo mstore(add(payload, 0x03e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // quotient_const - mstore(add(payload, 0x0400), 0x0000000000000000000000000000000000000000000000000000040000000000) // quotient_const - mstore(add(payload, 0x0420), 0x0000000000000000000000000000000000000000000000000000000000100000) // quotient_const + mstore(add(payload, 0x0400), 0x0000000000000000000000000000000000000000000000000000000000100000) // quotient_const + mstore(add(payload, 0x0420), 0x0000000000000000000000000000000000000000000000000000040000000000) // quotient_const mstore(add(payload, 0x0440), 0x0000000000000000000000000000000000000000000000000000000000000002) // quotient_const mstore(add(payload, 0x0460), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffefff00001) // quotient_const mstore(add(payload, 0x0480), 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfefffffffeffe00001) // quotient_const @@ -119,19 +122,19 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x06c0), 0x0000000000000000000000000000000000000000000000000000000000040011) // quotient_const mstore(add(payload, 0x06e0), 0x0000000000000000000000000000000000000000000000000000001100000000) // quotient_const mstore(add(payload, 0x0700), 0x0000000000000000000000000000000000000000000000000000000044000000) // quotient_const - mstore(add(payload, 0x0720), 0x0000000000000000000000000000000000000000000000000000000000200000) // quotient_const - mstore(add(payload, 0x0740), 0x0000000000000000000000000000000000000000000000000000000000000400) // quotient_const - mstore(add(payload, 0x0760), 0x0000000000000000000000000000000000000000000000000000000000400000) // quotient_const + mstore(add(payload, 0x0720), 0x0000000000000000000000000000000000000000000000000000000000000400) // quotient_const + mstore(add(payload, 0x0740), 0x0000000000000000000000000000000000000000000000000000000000200000) // quotient_const + mstore(add(payload, 0x0760), 0x0000000000000000000000000000000000000000000000000000000000000004) // quotient_const mstore(add(payload, 0x0780), 0x0000000000000000000000000000000000000000000000000000000000002000) // quotient_const - mstore(add(payload, 0x07a0), 0x0000000000000000000000000000000000000000000000000000000000000004) // quotient_const - mstore(add(payload, 0x07c0), 0x0000000000000000000000000000000000000000000000000000100000000000) // quotient_const + mstore(add(payload, 0x07a0), 0x0000000000000000000000000000000000000000000000000000000000400000) // quotient_const + mstore(add(payload, 0x07c0), 0x0000000000000000000000000000000000000000000000000000000000000010) // quotient_const mstore(add(payload, 0x07e0), 0x0000000000000000000000000000000000000000000000000000000004000000) // quotient_const - mstore(add(payload, 0x0800), 0x0000000000000000000000000000000000000000000000000000000000000010) // quotient_const - mstore(add(payload, 0x0820), 0x0000000000000000000000000000000000000000000000000000000002000000) // quotient_const + mstore(add(payload, 0x0800), 0x0000000000000000000000000000000000000000000000000000100000000000) // quotient_const + mstore(add(payload, 0x0820), 0x0000000000000000000000000000000000000000000000000000000000000040) // quotient_const mstore(add(payload, 0x0840), 0x0000000000000000000000000000000000000000000000000000000000000800) // quotient_const - mstore(add(payload, 0x0860), 0x0000000000000000000000000000000000000000000000000000000000000040) // quotient_const - mstore(add(payload, 0x0880), 0x0000000000000000000000000000000000000000000000000004000000000000) // quotient_const - mstore(add(payload, 0x08a0), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x0860), 0x0000000000000000000000000000000000000000000000000000000002000000) // quotient_const + mstore(add(payload, 0x0880), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x08a0), 0x0000000000000000000000000000000000000000000000000004000000000000) // quotient_const mstore(add(payload, 0x08c0), 0x0000000000000000000000000000000000000000000000000000000000040000) // quotient_const mstore(add(payload, 0x08e0), 0x0000000000000000000000000000000000000000000000000000000000000080) // quotient_const mstore(add(payload, 0x0900), 0x0000000000000000000000000000000000000000000000000000000000000008) // quotient_const @@ -141,25 +144,25 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x0980), 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140) // quotient_const mstore(add(payload, 0x09a0), 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a) // quotient_const mstore(add(payload, 0x09c0), 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db) // quotient_const - mstore(add(payload, 0x09e0), 0x055b80105ea0055c2008060d000b020001055ca0105cc0105ba00901115d0013) // quotient_program - mstore(add(payload, 0x0a00), 0x5b6002105c000901115ce0135b4002105be00d030608060d000b030000055ca0) // quotient_program - mstore(add(payload, 0x0a20), 0x105cc00901115d00135b6002105c000901115ce0135b4002105be00d03060806) // quotient_program - mstore(add(payload, 0x0a40), 0x0d000b040000055b80105ba0055bc008060d000b0400011b00001b0001210001) // quotient_program - mstore(add(payload, 0x0a60), 0x00000700045b40055b60065b80075ba0085bc0095be00a5c000b5c200c5ca00d) // quotient_program - mstore(add(payload, 0x0a80), 0x5cc00e5ce00f5d00105d20115d400b07000021000100000700045b40055b6012) // quotient_program - mstore(add(payload, 0x0aa0), 0x5b80135ba0145bc0095be00a5c00155c20165ca0175cc00e5ce00f5d00185d20) // quotient_program - mstore(add(payload, 0x0ac0), 0x195d400b080000091a115d60135c601b105d80055da008060d000b090000091c) // quotient_program - mstore(add(payload, 0x0ae0), 0x115d60135dc01d135c601e105c80055da008060d000b0a0000091f115ce0135d) // quotient_program - mstore(add(payload, 0x0b00), 0x0020135b4021105b60055b8008060d000b0a00010922115d60135dc01d135c60) // quotient_program - mstore(add(payload, 0x0b20), 0x23135c8024105d80055da008060d000b0b00000925115ce0135d0020135b401c) // quotient_program - mstore(add(payload, 0x0b40), 0x135b6026105be0055b8008060d000b0b00011c275c40285c60295c80025d601b) // quotient_program - mstore(add(payload, 0x0b60), 0x5dc02a5de02b5e00105d80055da008060d000b0c0000090008105de0115de00d) // quotient_program - mstore(add(payload, 0x0b80), 0x000b0c0001090008105c40115c400d000b0c0001090008105e00115e000d000b) // quotient_program - mstore(add(payload, 0x0ba0), 0x0c00011c005ba0005bc0005c20005ca0005cc0005d20005d40055da0135e202c) // quotient_program - mstore(add(payload, 0x0bc0), 0x08060d000b0d0000055b40115b40115b40055ba008060d000b0e0000055b6011) // quotient_program - mstore(add(payload, 0x0be0), 0x5b60115b60055bc008060d000b0e0001055b80115b80115b80055c4008060d00) // quotient_program - mstore(add(payload, 0x0c00), 0x0b0e00011b00021b0003055c2008105f40055b40115b40115ba00d2d06055b60) // quotient_program - mstore(add(payload, 0x0c20), 0x115b60115bc00d2e06055b80115b80115c400d2f060d000b0e0001191f000000) // quotient_program + mstore(add(payload, 0x09e0), 0x056b00106e20056ba008060d000b020001056c20106c40106b200902116c8013) // quotient_program + mstore(add(payload, 0x0a00), 0x6ae001106b800902116c60136ac001106b600d030608060d000b030000056c20) // quotient_program + mstore(add(payload, 0x0a20), 0x106c400902116c80136ae001106b800902116c60136ac001106b600d03060806) // quotient_program + mstore(add(payload, 0x0a40), 0x0d000b040000056b00106b20056b4008060d000b0400011b00001b0001210001) // quotient_program + mstore(add(payload, 0x0a60), 0x00000700046ac0056ae0066b00076b20086b40096b600a6b800b6ba00c6c200d) // quotient_program + mstore(add(payload, 0x0a80), 0x6c400e6c600f6c80106ca0116cc00b07000021000100000700046ac0056ae012) // quotient_program + mstore(add(payload, 0x0aa0), 0x6b00136b20146b40096b600a6b80156ba0166c20176c400e6c600f6c80186ca0) // quotient_program + mstore(add(payload, 0x0ac0), 0x196cc00b080000091b116ce0136be01a106d00056d2008060d000b090000091e) // quotient_program + mstore(add(payload, 0x0ae0), 0x116ce0136d401d136be01c106c00056d2008060d000b0a00000921116c60136c) // quotient_program + mstore(add(payload, 0x0b00), 0x8020136ac01f106ae0056b0008060d000b0a00010924116ce0136d401d136be0) // quotient_program + mstore(add(payload, 0x0b20), 0x23136c0022106d00056d2008060d000b0b00000926116c60136c8020136ac01e) // quotient_program + mstore(add(payload, 0x0b40), 0x136ae025106b60056b0008060d000b0b00011c276bc0286be0296c00016ce01a) // quotient_program + mstore(add(payload, 0x0b60), 0x6d402a6d602b6d80106d00056d2008060d000b0c0000090008106d60116d600d) // quotient_program + mstore(add(payload, 0x0b80), 0x000b0c0001090008106bc0116bc00d000b0c0001090008106d80116d800d000b) // quotient_program + mstore(add(payload, 0x0ba0), 0x0c00011c006b20006b40006ba0006c20006c40006ca0006cc0056d20136da02c) // quotient_program + mstore(add(payload, 0x0bc0), 0x08060d000b0d0000056ac0116ac0116ac0056b2008060d000b0e0000056ae011) // quotient_program + mstore(add(payload, 0x0be0), 0x6ae0116ae0056b4008060d000b0e0001056b00116b00116b00056bc008060d00) // quotient_program + mstore(add(payload, 0x0c00), 0x0b0e00011b00021b0003056ba008106ec0056ac0116ac0116b200d2d06056ae0) // quotient_program + mstore(add(payload, 0x0c20), 0x116ae0116b400d2e06056b00116b00116bc00d2f060d000b0e0001191f000000) // quotient_program // Fixed-column commitment 0, stored as one // EIP-2537 padded uncompressed G1 slot. mstore(add(payload, 0x0c40), 0x0000000000000000000000000000000002586b66bd923976cc5f5c9ff774e3c2) // fixed_comms[0].x_hi diff --git a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2QuotientEvaluator.sol b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2QuotientEvaluator.sol index 6e74608d2..30464e1e4 100644 --- a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2QuotientEvaluator.sol +++ b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2QuotientEvaluator.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Split Halo2 quotient numerator evaluator. /// @notice Reconstructs the scalar side of the linearization query for a generated verifier. @@ -47,53 +50,53 @@ contract Halo2QuotientEvaluator { // Start of the copied verifier-key payload in memory. The VK payload also // carries the compact quotient VM constant/program tables used by the // included numerator block. - uint256 internal constant VK_MPTR = 0x2700; + uint256 internal constant VK_MPTR = 0x3680; // Fiat-Shamir challenge slots. Halo2Verifier sampled these in transcript // order before the external call. The evaluator only reads them. - uint256 internal constant CHALLENGE_MPTR = 0x6980; - uint256 internal constant THETA_MPTR = 0x6980; - uint256 internal constant BETA_MPTR = 0x69a0; - uint256 internal constant GAMMA_MPTR = 0x69c0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x69e0; - uint256 internal constant Y_MPTR = 0x6a00; - uint256 internal constant X_MPTR = 0x6a20; - uint256 internal constant X1_MPTR = 0x6a40; - uint256 internal constant X2_MPTR = 0x6a60; - uint256 internal constant X3_MPTR = 0x6a80; - uint256 internal constant X4_MPTR = 0x6aa0; + uint256 internal constant CHALLENGE_MPTR = 0x7900; + uint256 internal constant THETA_MPTR = 0x7900; + uint256 internal constant BETA_MPTR = 0x7920; + uint256 internal constant GAMMA_MPTR = 0x7940; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x7960; + uint256 internal constant Y_MPTR = 0x7980; + uint256 internal constant X_MPTR = 0x79a0; + uint256 internal constant X1_MPTR = 0x79c0; + uint256 internal constant X2_MPTR = 0x79e0; + uint256 internal constant X3_MPTR = 0x7a00; + uint256 internal constant X4_MPTR = 0x7a20; // Common polynomial values at x. Halo2Verifier computes these once after // sampling x and places them in the frame so the numerator block can share // the exact Rust verifier inputs. - uint256 internal constant X_N_MPTR = 0x6cc0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x6ce0; - uint256 internal constant L_LAST_MPTR = 0x6d00; - uint256 internal constant L_BLIND_MPTR = 0x6d20; - uint256 internal constant L_0_MPTR = 0x6d40; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x6d60; - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x6d80; + uint256 internal constant X_N_MPTR = 0x7c40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x7c60; + uint256 internal constant L_LAST_MPTR = 0x7c80; + uint256 internal constant L_BLIND_MPTR = 0x7ca0; + uint256 internal constant L_0_MPTR = 0x7cc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x7ce0; + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x7d00; // Proof evaluation table. Values are already decoded as canonical Fr words // by Halo2Verifier. The generated numerator code indexes this table by the // same query order as the Rust verifier. - uint256 internal constant REVERSED_EVALS_MPTR = 0x8500; + uint256 internal constant REVERSED_EVALS_MPTR = 0x9480; // Scratch/output region for simple-selector linearization accumulators. // The numerator block writes one bucket per simple selector, then the // fallback copies those buckets into the compact return frame. - uint256 internal constant SELECTOR_ACC_MPTR = 0xa1c0; + uint256 internal constant SELECTOR_ACC_MPTR = 0xb140; // Callee-local scratch for trace hooks. Trace-enabled verifier builds call // this evaluator with CALL so quotient identity logs can be compared with // the native Rust trace. Production verifier builds keep using STATICCALL // and render this evaluator without trace hooks. - uint256 internal constant TRACE_U256_MPTR = 0x80; - uint256 internal constant QUOTIENT_OUTPUT_MPTR = 0x80; + uint256 internal constant TRACE_U256_MPTR = 0x1000; + uint256 internal constant QUOTIENT_OUTPUT_MPTR = 0x1000; // External-call frame metadata. The main verifier calls this contract with // exactly QUOTIENT_FRAME_LEN bytes starting at // QUOTIENT_FRAME_BASE, then checks the return length and QUOTIENT_MAGIC. - uint256 internal constant QUOTIENT_FRAME_BASE = 0x2700; + uint256 internal constant QUOTIENT_FRAME_BASE = 0x3680; uint256 internal constant QUOTIENT_FRAME_LEN = 0x6ac0; uint256 internal constant QUOTIENT_OUTPUT_LEN = 0x0180; uint256 internal constant QUOTIENT_MAGIC = 0x00000000000000000000000000000000000000000000000051554556414c0001; @@ -135,7 +138,17 @@ contract Halo2QuotientEvaluator { // block mirrors that rule by accumulating those identities into // SELECTOR_ACC_MPTR buckets for later multiplication by fixed // selector commitments, while fully evaluated identities contribute - // to the negated expected scalar. // Optional quotient helper functions. Each one is rendered only + // to the negated expected scalar. // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } + + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr // helpers and share the same FR_MODULUS as the surrounding @@ -155,7 +168,8 @@ contract Halo2QuotientEvaluator { let q_r := FR_MODULUS let x2 := mulmod(x, x, q_r) z := mulmod(x, mulmod(x2, x2, q_r), q_r) - } // =============================================================== + } + // =============================================================== // Batched identity numerator / linearization target. // // This block does not evaluate the quotient polynomial h(x), and @@ -251,15 +265,15 @@ contract Halo2QuotientEvaluator { // q_const_mptr points to Fr constants used by the VM. // q_program_mptr points to the bytecode stream. // Constants are stored as consecutive 32-byte Fr words. - let q_const_mptr := 0x2ae0 + let q_const_mptr := 0x3a60 // Program bytes are also stored in the VK payload, packed into // 32-byte words by PackedProgramCodec. - let q_program_mptr := 0x4120 + let q_program_mptr := 0x50a0 // Running Horner accumulator for fully evaluated identities. // After all identities, this is nu_y(x) for the `None` // identity group. // Initialize A = 0 before scanning the identity stream. - mstore(0xa300, 0) + mstore(0xb280, 0) // Simple selectors are grouped into separate linearization // buckets. They start at zero for every proof. // q_sel_zero_off walks selector bucket byte offsets. @@ -274,12 +288,19 @@ contract Halo2QuotientEvaluator { { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0xb2c0, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, 49) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) // Store y^i at selector_power_mptr + 32*i. - mstore(add(0xa340, shl(5, q_y_power_i)), q_y_power) + mstore(add(0xb2c0, shl(5, q_y_power_i)), q_y_power) } } @@ -288,102 +309,102 @@ contract Halo2QuotientEvaluator { // VM/native identities, so they occupy the same y-batch order. { let var0 := 0x1 - let f_3 := mload(0x8b40) - let f_4 := mload(0x8a40) - let a_0 := mload(0x8520) + let f_3 := mload(0x9ac0) + let f_4 := mload(0x99c0) + let a_0 := mload(0x94a0) let var1 := mulmod(f_4, a_0, r) let var2 := addmod(f_3, var1, r) - let f_5 := mload(0x8a60) - let a_1 := mload(0x8540) + let f_5 := mload(0x99e0) + let a_1 := mload(0x94c0) let var3 := mulmod(f_5, a_1, r) let var4 := addmod(var2, var3, r) - let f_6 := mload(0x8a80) - let a_2 := mload(0x8560) + let f_6 := mload(0x9a00) + let a_2 := mload(0x94e0) let var5 := mulmod(f_6, a_2, r) let var6 := addmod(var4, var5, r) - let f_7 := mload(0x8aa0) - let a_3 := mload(0x8580) + let f_7 := mload(0x9a20) + let a_3 := mload(0x9500) let var7 := mulmod(f_7, a_3, r) let var8 := addmod(var6, var7, r) - let f_8 := mload(0x8ac0) - let a_4 := mload(0x85a0) + let f_8 := mload(0x9a40) + let a_4 := mload(0x9520) let var9 := mulmod(f_8, a_4, r) let var10 := addmod(var8, var9, r) - let f_0 := mload(0x8ae0) - let a_0_next_1 := mload(0x85c0) + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) let var11 := mulmod(f_0, a_0_next_1, r) let var12 := addmod(var10, var11, r) - let f_1 := mload(0x8b00) + let f_1 := mload(0x9a80) let var13 := mulmod(f_1, a_0, r) let var14 := mulmod(var13, a_1, r) let var15 := addmod(var12, var14, r) - let f_2 := mload(0x8b20) + let f_2 := mload(0x9aa0) let var16 := mulmod(f_2, a_0, r) let var17 := mulmod(var16, a_2, r) let var18 := addmod(var15, var17, r) let var19 := mulmod(var0, var18, r) - mstore(0xa960, var19) + mstore(0xb8e0, var19) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } { let var0 := 0x1 - let a_1 := mload(0x8540) - let a_2 := mload(0x8560) + let a_1 := mload(0x94c0) + let a_2 := mload(0x94e0) let var1 := addmod(a_1, a_2, r) - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var2 := addmod(0, sub(r, a_3), r) let var3 := addmod(var1, var2, r) - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var4 := addmod(0, sub(r, a_4), r) let var5 := addmod(var3, var4, r) let var6 := mulmod(var0, var5, r) - mstore(0xa960, var6) + mstore(0xb8e0, var6) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } { let var0 := 0x1 - let a_0 := mload(0x8520) - let f_4 := mload(0x8a40) + let a_0 := mload(0x94a0) + let f_4 := mload(0x99c0) let var1 := addmod(a_0, f_4, r) - let a_0_next_1 := mload(0x85c0) + let a_0_next_1 := mload(0x9540) let var2 := addmod(0, sub(r, a_0_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0xa960, var4) + mstore(0xb8e0, var4) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } { let var0 := 0x1 - let a_1 := mload(0x8540) - let f_5 := mload(0x8a60) + let a_1 := mload(0x94c0) + let f_5 := mload(0x99e0) let var1 := addmod(a_1, f_5, r) - let a_1_next_1 := mload(0x85e0) + let a_1_next_1 := mload(0x9560) let var2 := addmod(0, sub(r, a_1_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0xa960, var4) + mstore(0xb8e0, var4) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } // VM registers: @@ -403,24 +424,27 @@ contract Halo2QuotientEvaluator { // q_end is an exclusive byte pointer for the VM loop. let q_end := add(q_program_mptr, 0x11cf) // q_sp starts at the first free stack word. - let q_sp := 0xa960 + let q_sp := 0xb8e0 // q_top is meaningless until q_has_top is set. let q_top := 0 // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity + // 0x21 modarith7 // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -444,6 +468,7 @@ contract Halo2QuotientEvaluator { // 64 KiB when this compact form is emitted. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } if q_has_top { mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) @@ -455,6 +480,7 @@ contract Halo2QuotientEvaluator { case 0x06 { // The safety validator guarantees a spilled operand // exists before ADD. q_top is the right operand. + if eq(q_sp, 0xb8e0) { q_program_fail() } q_sp := sub(q_sp, 0x20) q_top := addmod(mload(q_sp), q_top, r) } @@ -478,6 +504,7 @@ contract Halo2QuotientEvaluator { // already range-checked Fr scalar in verifier memory. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } q_top := addmod(q_top, mload(q_ptr), r) } // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. @@ -485,6 +512,7 @@ contract Halo2QuotientEvaluator { // In-place multiply by a planned memory word. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } q_top := mulmod(q_top, mload(q_ptr), r) } // Limb-aware opcodes are opt-in compact forms for @@ -531,6 +559,7 @@ contract Halo2QuotientEvaluator { // whole identity is gated by mload(q_cond_ptr). q_cond_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_cond_ptr, 0x3680), 0x6aa0) { q_program_fail() } } let q_acc := 0 @@ -565,6 +594,7 @@ contract Halo2QuotientEvaluator { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -577,12 +607,14 @@ contract Halo2QuotientEvaluator { for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { let q_lhs := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } let q_lhs_value := mload(q_lhs) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { let q_word := mload(q_pc) let qconst := byte(0, q_word) let q_rhs := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -602,6 +634,8 @@ contract Halo2QuotientEvaluator { let q_lhs_base := shr(240, q_pair_word) let q_rhs_base := and(shr(224, q_pair_word), 0xffff) q_pc := add(q_pc, 0x04) + if gt(sub(q_lhs_base, 0x3680), 0x69e0) { q_program_fail() } + if gt(sub(q_rhs_base, 0x3680), 0x69e0) { q_program_fail() } let q_coeff_pc := q_pc q_pc := add(q_pc, 13) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { @@ -627,6 +661,7 @@ contract Halo2QuotientEvaluator { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x3680), 0x6aa0) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -641,6 +676,8 @@ contract Halo2QuotientEvaluator { let q_lhs := and(shr(232, q_word), 0xffff) let q_rhs := and(shr(216, q_word), 0xffff) q_pc := add(q_pc, 5) + if gt(sub(q_lhs, 0x3680), 0x6aa0) { q_program_fail() } + if gt(sub(q_rhs, 0x3680), 0x6aa0) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -677,82 +714,82 @@ contract Halo2QuotientEvaluator { // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := 0xa960 + q_sp := 0xb8e0 // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. { let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 - let q_perm_vals := 0xa960 - let q_perm_sigmas := 0xaba0 - let q_perm_z_cur := 0xade0 - let q_perm_z_next := 0xaea0 - let q_perm_z_last := 0xaf60 - let q_perm_delta_base_ptr := 0xb000 + let q_perm_vals := 0xb8e0 + let q_perm_sigmas := 0xbb20 + let q_perm_z_cur := 0xbd60 + let q_perm_z_next := 0xbe20 + let q_perm_z_last := 0xbee0 + let q_perm_delta_base_ptr := 0xbf80 let q_perm_num_cols := 18 let q_perm_num_sets := 6 let q_perm_chunk_len := 3 let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 - mstore(add(q_perm_vals, 0x0), mload(0x8a20)) + mstore(add(q_perm_vals, 0x0), mload(0x99a0)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x8520, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x94a0, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0xc0), mload(0x8500)) + mstore(add(q_perm_vals, 0xc0), mload(0x9480)) mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 9) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x100), q_perm_val_load_dst_off), mload(add(0x8620, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x100), q_perm_val_load_dst_off), mload(add(0x95a0, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0x220), mload(0x8a00)) + mstore(add(q_perm_vals, 0x220), mload(0x9980)) { for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 18) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off - mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x8c40, q_perm_sigma_load_src_off))) + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x9bc0, q_perm_sigma_load_src_off))) } } { for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 6) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) - mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x8e80, q_perm_z_cur_load_src_off))) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x9e00, q_perm_z_cur_load_src_off))) } } { for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 6) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) - mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x8ea0, q_perm_z_next_load_src_off))) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x9e20, q_perm_z_next_load_src_off))) } } { for { let q_perm_z_last_load_i := 0 } lt(q_perm_z_last_load_i, 5) { q_perm_z_last_load_i := add(q_perm_z_last_load_i, 1) } { let q_perm_z_last_load_dst_off := shl(5, q_perm_z_last_load_i) let q_perm_z_last_load_src_off := mul(q_perm_z_last_load_i, 0x60) - mstore(add(add(q_perm_z_last, 0x0), q_perm_z_last_load_dst_off), mload(add(0x8ec0, q_perm_z_last_load_src_off))) + mstore(add(add(q_perm_z_last, 0x0), q_perm_z_last_load_dst_off), mload(add(0x9e40, q_perm_z_last_load_src_off))) } } let q_perm_eval := 0 q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) let q_perm_zn := mload(add(q_perm_z_cur, 0xa0)) q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) for { let q_perm_i := 1 } lt(q_perm_i, 6) { q_perm_i := add(q_perm_i, 1) } { let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) } mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) for { let q_perm_set := 0 } lt(q_perm_set, 6) { q_perm_set := add(q_perm_set, 1) } { @@ -771,8 +808,8 @@ contract Halo2QuotientEvaluator { q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) } q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_perm_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_perm_eval, r)) mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) } } @@ -793,13 +830,13 @@ contract Halo2QuotientEvaluator { // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := 0xa960 + q_sp := 0xb8e0 // Generated LogUp code follows the same y-batch order // as the Rust identity stream. { - let q_lookup_f := 0xa960 - let q_lookup_prefix := 0xa9e0 - let q_lookup_suffix := 0xaa60 + let q_lookup_f := 0xb8e0 + let q_lookup_prefix := 0xb960 + let q_lookup_suffix := 0xb9e0 let q_lookup_l0 := mload(L_0_MPTR) let q_lookup_llast := mload(L_LAST_MPTR) let q_lookup_lblind := mload(L_BLIND_MPTR) @@ -809,17 +846,17 @@ contract Halo2QuotientEvaluator { let q_lookup_theta := mload(THETA_MPTR) { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x90e0), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa060), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } { - let f_10 := mload(0x8b60) + let f_10 := mload(0x9ae0) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) let var1 := mulmod(var0, q_lookup_theta, r) for { let q_lookup_shared_i := 0 } lt(q_lookup_shared_i, 4) { q_lookup_shared_i := add(q_lookup_shared_i, 1) } { let q_lookup_shared_off := shl(5, q_lookup_shared_i) - let q_lookup_shared_tail := mload(add(0x8540, q_lookup_shared_off)) + let q_lookup_shared_tail := mload(add(0x94c0, q_lookup_shared_off)) let q_lookup_shared_compressed := addmod(var1, q_lookup_shared_tail, r) mstore(add(q_lookup_f, q_lookup_shared_off), addmod(q_lookup_shared_compressed, q_lookup_beta, r)) } @@ -841,130 +878,130 @@ contract Halo2QuotientEvaluator { for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 4) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) } - let q_lookup_eval := addmod(mulmod(mload(0x90c0), q_lookup_product, r), sub(r, q_lookup_sum), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0xa040), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x90c0) - let f_17 := mload(0x8be0) - let f_11 := mload(0x8b80) + let q_lookup_sum_h := mload(0xa040) + let f_17 := mload(0x9b60) + let f_11 := mload(0x9b00) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) - let f_12 := mload(0x8ba0) + let f_12 := mload(0x9b20) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) let q_lookup_s_sum_h := mulmod(f_17, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x9100), sub(r, addmod(mload(0x90e0), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0xa080), sub(r, addmod(mload(0xa060), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x90a0), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa020), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } } { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x9160), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0xa0e0), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } { - let a_14 := mload(0x8a00) + let a_14 := mload(0x9980) let var0 := addmod(mulmod(0, q_lookup_theta, r), a_14, r) - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_0, r) - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_1, r) - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var3 := addmod(mulmod(var2, q_lookup_theta, r), a_2, r) - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_3, r) - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_4, r) - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var6 := addmod(mulmod(var5, q_lookup_theta, r), a_5, r) - let a_6 := mload(0x8640) + let a_6 := mload(0x95c0) let var7 := addmod(mulmod(var6, q_lookup_theta, r), a_6, r) - let a_7 := mload(0x8660) + let a_7 := mload(0x95e0) let var8 := addmod(mulmod(var7, q_lookup_theta, r), a_7, r) - let a_8 := mload(0x8680) + let a_8 := mload(0x9600) let var9 := addmod(mulmod(var8, q_lookup_theta, r), a_8, r) - let a_9 := mload(0x86a0) + let a_9 := mload(0x9620) let var10 := addmod(mulmod(var9, q_lookup_theta, r), a_9, r) - let a_10 := mload(0x86c0) + let a_10 := mload(0x9640) let var11 := addmod(mulmod(var10, q_lookup_theta, r), a_10, r) - let a_11 := mload(0x86e0) + let a_11 := mload(0x9660) let var12 := addmod(mulmod(var11, q_lookup_theta, r), a_11, r) - let a_12 := mload(0x8700) + let a_12 := mload(0x9680) let var13 := addmod(mulmod(var12, q_lookup_theta, r), a_12, r) - let a_13 := mload(0x8720) + let a_13 := mload(0x96a0) let var14 := addmod(mulmod(var13, q_lookup_theta, r), a_13, r) - let f_13 := mload(0x8bc0) + let f_13 := mload(0x9b40) let var15 := addmod(mulmod(var14, q_lookup_theta, r), f_13, r) - let q_lookup_eval := addmod(mulmod(mload(0x9140), addmod(var15, q_lookup_beta, r), r), sub(r, 1), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0xa0c0), addmod(var15, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x9140) + let q_lookup_sum_h := mload(0xa0c0) let var0 := 0x1 - let f_26 := mload(0x8c20) + let f_26 := mload(0x9ba0) let var1 := addmod(0, sub(r, f_26), r) let var2 := addmod(var0, var1, r) - let a_14 := mload(0x8a00) + let a_14 := mload(0x9980) let var3 := mulmod(var2, a_14, r) let var4 := addmod(mulmod(0, q_lookup_theta, r), var3, r) - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var5 := mulmod(var2, a_0, r) let var6 := addmod(mulmod(var4, q_lookup_theta, r), var5, r) - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var7 := mulmod(var2, a_1, r) let var8 := addmod(mulmod(var6, q_lookup_theta, r), var7, r) - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var9 := mulmod(var2, a_2, r) let var10 := addmod(mulmod(var8, q_lookup_theta, r), var9, r) - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var11 := mulmod(var2, a_3, r) let var12 := addmod(mulmod(var10, q_lookup_theta, r), var11, r) - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var13 := mulmod(var2, a_4, r) let var14 := addmod(mulmod(var12, q_lookup_theta, r), var13, r) - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var15 := mulmod(var2, a_5, r) let var16 := addmod(mulmod(var14, q_lookup_theta, r), var15, r) - let a_6 := mload(0x8640) + let a_6 := mload(0x95c0) let var17 := mulmod(var2, a_6, r) let var18 := addmod(mulmod(var16, q_lookup_theta, r), var17, r) - let a_7 := mload(0x8660) + let a_7 := mload(0x95e0) let var19 := mulmod(var2, a_7, r) let var20 := addmod(mulmod(var18, q_lookup_theta, r), var19, r) - let a_8 := mload(0x8680) + let a_8 := mload(0x9600) let var21 := mulmod(var2, a_8, r) let var22 := addmod(mulmod(var20, q_lookup_theta, r), var21, r) - let a_9 := mload(0x86a0) + let a_9 := mload(0x9620) let var23 := mulmod(var2, a_9, r) let var24 := addmod(mulmod(var22, q_lookup_theta, r), var23, r) - let a_10 := mload(0x86c0) + let a_10 := mload(0x9640) let var25 := mulmod(var2, a_10, r) let var26 := addmod(mulmod(var24, q_lookup_theta, r), var25, r) - let a_11 := mload(0x86e0) + let a_11 := mload(0x9660) let var27 := mulmod(var2, a_11, r) let var28 := addmod(mulmod(var26, q_lookup_theta, r), var27, r) - let a_12 := mload(0x8700) + let a_12 := mload(0x9680) let var29 := mulmod(var2, a_12, r) let var30 := addmod(mulmod(var28, q_lookup_theta, r), var29, r) - let a_13 := mload(0x8720) + let a_13 := mload(0x96a0) let var31 := mulmod(var2, a_13, r) let var32 := addmod(mulmod(var30, q_lookup_theta, r), var31, r) - let f_13 := mload(0x8bc0) + let f_13 := mload(0x9b40) let var33 := mulmod(var2, f_13, r) let var34 := addmod(mulmod(var32, q_lookup_theta, r), var33, r) let q_lookup_s_sum_h := mulmod(var0, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x9180), sub(r, addmod(mload(0x9160), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0xa100), sub(r, addmod(mload(0xa0e0), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var34, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x9120), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0xa0a0), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_lookup_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_lookup_eval, r)) } } } @@ -985,104 +1022,104 @@ contract Halo2QuotientEvaluator { // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := 0xa960 + q_sp := 0xb8e0 // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx case 0 { { let var0 := 0x1 - let f_0 := mload(0x8ae0) - let a_0_next_1 := mload(0x85c0) + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) let var1 := addmod(0, sub(r, a_0_next_1), r) let var2 := addmod(f_0, var1, r) let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0xa960, var18) + mstore(0xb8e0, var18) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } } case 1 { { let var0 := 0x1 - let f_1 := mload(0x8b00) - let a_1_next_1 := mload(0x85e0) + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) let var1 := addmod(0, sub(r, a_1_next_1), r) let var2 := addmod(f_1, var1, r) let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0xa960, var18) + mstore(0xb8e0, var18) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0xa340, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0xb2c0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } } case 2 { { let var0 := 0x1 - let a_0 := mload(0x8520) - let a_0_next_1 := mload(0x85c0) + let a_0 := mload(0x94a0) + let a_0_next_1 := mload(0x9540) let var1 := mulmod(a_0, a_0_next_1, r) let var2 := 0x100000000000000 - let a_1_next_1 := mload(0x85e0) + let a_1_next_1 := mload(0x9560) let var3 := mulmod(a_0, a_1_next_1, r) let var4 := mulmod(var2, var3, r) let var5 := addmod(var1, var4, r) let var6 := 0x10000000000000000000000000000 - let a_2_next_1 := mload(0x8600) + let a_2_next_1 := mload(0x9580) let var7 := mulmod(a_0, a_2_next_1, r) let var8 := mulmod(var6, var7, r) let var9 := addmod(var5, var8, r) - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var10 := mulmod(a_1, a_0_next_1, r) let var11 := mulmod(var2, var10, r) let var12 := addmod(var9, var11, r) @@ -1090,15 +1127,15 @@ contract Halo2QuotientEvaluator { let var14 := mulmod(var6, var13, r) let var15 := addmod(var12, var14, r) let var16 := 0x3212e00cde6d2002b119d800000347fcb8 - let a_6_next_1 := mload(0x87a0) + let a_6_next_1 := mload(0x9720) let var17 := mulmod(a_1, a_6_next_1, r) let var18 := mulmod(var16, var17, r) let var19 := addmod(var15, var18, r) - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var20 := mulmod(a_2, a_0_next_1, r) let var21 := mulmod(var6, var20, r) let var22 := addmod(var19, var21, r) - let a_5_next_1 := mload(0x8780) + let a_5_next_1 := mload(0x9700) let var23 := mulmod(a_2, a_5_next_1, r) let var24 := mulmod(var16, var23, r) let var25 := addmod(var22, var24, r) @@ -1106,8 +1143,8 @@ contract Halo2QuotientEvaluator { let var27 := mulmod(a_2, a_6_next_1, r) let var28 := mulmod(var26, var27, r) let var29 := addmod(var25, var28, r) - let a_3 := mload(0x8580) - let a_4_next_1 := mload(0x8760) + let a_3 := mload(0x9500) + let a_4_next_1 := mload(0x96e0) let var30 := mulmod(a_3, a_4_next_1, r) let var31 := mulmod(var16, var30, r) let var32 := addmod(var29, var31, r) @@ -1118,8 +1155,8 @@ contract Halo2QuotientEvaluator { let var37 := mulmod(a_3, a_6_next_1, r) let var38 := mulmod(var36, var37, r) let var39 := addmod(var35, var38, r) - let a_4 := mload(0x85a0) - let a_3_next_1 := mload(0x8740) + let a_4 := mload(0x9520) + let a_3_next_1 := mload(0x96c0) let var40 := mulmod(a_4, a_3_next_1, r) let var41 := mulmod(var16, var40, r) let var42 := addmod(var39, var41, r) @@ -1133,7 +1170,7 @@ contract Halo2QuotientEvaluator { let var50 := mulmod(a_4, a_6_next_1, r) let var51 := mulmod(var49, var50, r) let var52 := addmod(var48, var51, r) - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var53 := mulmod(a_5, a_2_next_1, r) let var54 := mulmod(var16, var53, r) let var55 := addmod(var52, var54, r) @@ -1150,7 +1187,7 @@ contract Halo2QuotientEvaluator { let var66 := mulmod(a_5, a_6_next_1, r) let var67 := mulmod(var65, var66, r) let var68 := addmod(var64, var67, r) - let a_6 := mload(0x8640) + let a_6 := mload(0x95c0) let var69 := mulmod(a_6, a_1_next_1, r) let var70 := mulmod(var16, var69, r) let var71 := addmod(var68, var70, r) @@ -1180,23 +1217,23 @@ contract Halo2QuotientEvaluator { let var95 := mulmod(var6, a_2_next_1, r) let var96 := addmod(var94, var95, r) let var97 := addmod(var92, var96, r) - let a_7 := mload(0x8660) - let a_8 := mload(0x8680) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) let var98 := mulmod(var2, a_8, r) let var99 := addmod(a_7, var98, r) - let a_9 := mload(0x86a0) + let a_9 := mload(0x9620) let var100 := mulmod(var6, a_9, r) let var101 := addmod(var99, var100, r) let var102 := addmod(0, sub(r, var101), r) let var103 := addmod(var97, var102, r) - let a_7_next_1 := mload(0x87c0) + let a_7_next_1 := mload(0x9740) let var104 := 0x241eabfffeb153ffffb9feffffffffaaab let var105 := mulmod(a_7_next_1, var104, r) let var106 := addmod(0, sub(r, var105), r) let var107 := addmod(var103, var106, r) let var108 := addmod(0, sub(r, var16), r) let var109 := addmod(var107, var108, r) - let a_8_next_1 := mload(0x87e0) + let a_8_next_1 := mload(0x9760) let var110 := 0x73eda753299d7d483339d80809a1d80553b9202d7ffe85d4800008bb20000001 let var111 := addmod(a_8_next_1, var110, r) let var112 := 0x4000000000000000000000000000000000 @@ -1204,42 +1241,42 @@ contract Halo2QuotientEvaluator { let var114 := addmod(0, sub(r, var113), r) let var115 := addmod(var109, var114, r) let var116 := mulmod(var0, var115, r) - mstore(0xa960, var116) + mstore(0xb8e0, var116) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x80) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } } case 3 { { let var0 := 0x1 - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var1 := 0x10000000000000000000000000000 let var2 := addmod(a_0, var1, r) let var3 := 0x100000000000000 - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var4 := addmod(a_1, var1, r) let var5 := mulmod(var3, var4, r) let var6 := addmod(var2, var5, r) - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var7 := addmod(a_2, var1, r) let var8 := mulmod(var1, var7, r) let var9 := addmod(var6, var8, r) - let a_7 := mload(0x8660) - let a_8 := mload(0x8680) + let a_7 := mload(0x95e0) + let a_8 := mload(0x9600) let var10 := mulmod(var3, a_8, r) let var11 := addmod(a_7, var10, r) - let a_9 := mload(0x86a0) + let a_9 := mload(0x9620) let var12 := mulmod(var1, a_9, r) let var13 := addmod(var11, var12, r) let var14 := addmod(0, sub(r, var13), r) let var15 := addmod(var9, var14, r) let var16 := addmod(0, sub(r, var1), r) let var17 := addmod(var15, var16, r) - let a_7_next_1 := mload(0x87c0) + let a_7_next_1 := mload(0x9740) let var18 := 0x241eabfffeb153ffffb9feffffffffaaab let var19 := mulmod(a_7_next_1, var18, r) let var20 := addmod(0, sub(r, var19), r) @@ -1247,7 +1284,7 @@ contract Halo2QuotientEvaluator { let var22 := 0xd9d44a30b019261257667fde3844a8cd6 let var23 := addmod(0, sub(r, var22), r) let var24 := addmod(var21, var23, r) - let a_8_next_1 := mload(0x87e0) + let a_8_next_1 := mload(0x9760) let var25 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5b6e855000003ab00002 let var26 := addmod(a_8_next_1, var25, r) let var27 := 0x4000000000000000000000000000000000 @@ -1255,16 +1292,16 @@ contract Halo2QuotientEvaluator { let var29 := addmod(0, sub(r, var28), r) let var30 := addmod(var24, var29, r) let var31 := mulmod(var0, var30, r) - mstore(0xa960, var31) + mstore(0xb8e0, var31) } - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xa0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xa960), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0xb8e0), r)) } } - default { revert(0, 0) } + default { q_program_fail() } } // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. case 0x0b { @@ -1276,6 +1313,11 @@ contract Halo2QuotientEvaluator { q_pc := add(q_pc, 3) let q_sel_idx := shr(16, q_selector_payload) let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 10)) { q_program_fail() } + if gt(q_sel_gap, 0x30) { q_program_fail() } let q_eval := q_top q_has_top := 0 // Simple-selector identity: keep the same y-batch @@ -1285,28 +1327,34 @@ contract Halo2QuotientEvaluator { // The global fully-evaluated accumulator is still // multiplied by y so later main identities land at the // same y powers as Rust's reverse fold. - mstore(0xa300, mulmod(mload(0xa300), y, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) let q_sel_acc := mload(q_target_ptr) if q_sel_gap { // Selector buckets are sparse in the global // identity stream. Precomputed y^gap advances only // this selector's local accumulator. - q_sel_acc := mulmod(q_sel_acc, mload(add(0xa340, shl(5, q_sel_gap))), r) + q_sel_acc := mulmod(q_sel_acc, mload(add(0xb2c0, shl(5, q_sel_gap))), r) } mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) } // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0xb8e0)) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -1318,52 +1366,52 @@ contract Halo2QuotientEvaluator { { let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) { - let f_0 := mload(0x8ae0) - let a_0_next_1 := mload(0x85c0) + let f_0 := mload(0x9a60) + let a_0_next_1 := mload(0x9540) let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 let var1 := mulmod(a_0_next_1, var0, r) let var2 := addmod(f_0, var1, r) let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 - let a_0 := mload(0x8520) + let a_0 := mload(0x94a0) let var4 := mulmod(var3, a_0, r) let var5 := addmod(var2, var4, r) let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f - let a_1 := mload(0x8540) + let a_1 := mload(0x94c0) let var7 := mulmod(var6, a_1, r) let var8 := addmod(var5, var7, r) let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 - let a_2 := mload(0x8560) + let a_2 := mload(0x94e0) let var10 := q_pow5(a_2) let var11 := mulmod(var9, var10, r) let var12 := addmod(var8, var11, r) let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 - let a_3 := mload(0x8580) + let a_3 := mload(0x9500) let var14 := q_pow5(a_3) let var15 := mulmod(var13, var14, r) let var16 := addmod(var12, var15, r) let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 - let a_4 := mload(0x85a0) + let a_4 := mload(0x9520) let var18 := q_pow5(a_4) let var19 := mulmod(var17, var18, r) let var20 := addmod(var16, var19, r) let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e - let a_5 := mload(0x8620) + let a_5 := mload(0x95a0) let var22 := q_pow5(a_5) let var23 := mulmod(var21, var22, r) let var24 := addmod(var20, var23, r) let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 - let a_6 := mload(0x8640) + let a_6 := mload(0x95c0) let var26 := q_pow5(a_6) let var27 := mulmod(var25, var26, r) let var28 := addmod(var24, var27, r) let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_7 := mload(0x8660) + let a_7 := mload(0x95e0) let var30 := q_pow5(a_7) let var31 := mulmod(var29, var30, r) let var32 := addmod(var28, var31, r) let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) - let f_1 := mload(0x8b00) - let a_1_next_1 := mload(0x85e0) + let f_1 := mload(0x9a80) + let a_1_next_1 := mload(0x9560) let var34 := mulmod(a_1_next_1, var0, r) let var35 := addmod(f_1, var34, r) let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d @@ -1391,7 +1439,7 @@ contract Halo2QuotientEvaluator { let var58 := mulmod(var57, var30, r) let var59 := addmod(var56, var58, r) let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) - let f_2 := mload(0x8b20) + let f_2 := mload(0x9aa0) let var61 := mulmod(a_3, var0, r) let var62 := addmod(f_2, var61, r) let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 @@ -1404,7 +1452,7 @@ contract Halo2QuotientEvaluator { let var70 := mulmod(var69, var10, r) let var71 := addmod(var68, var70, r) let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) - let f_3 := mload(0x8b40) + let f_3 := mload(0x9ac0) let var73 := mulmod(a_4, var0, r) let var74 := addmod(f_3, var73, r) let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 @@ -1419,7 +1467,7 @@ contract Halo2QuotientEvaluator { let var84 := mulmod(var69, var14, r) let var85 := addmod(var83, var84, r) let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) - let f_4 := mload(0x8a40) + let f_4 := mload(0x99c0) let var87 := mulmod(a_5, var0, r) let var88 := addmod(f_4, var87, r) let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b @@ -1436,7 +1484,7 @@ contract Halo2QuotientEvaluator { let var100 := mulmod(var69, var18, r) let var101 := addmod(var99, var100, r) let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) - let f_5 := mload(0x8a60) + let f_5 := mload(0x99e0) let var103 := mulmod(a_6, var0, r) let var104 := addmod(f_5, var103, r) let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 @@ -1455,7 +1503,7 @@ contract Halo2QuotientEvaluator { let var118 := mulmod(var69, var22, r) let var119 := addmod(var117, var118, r) let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) - let f_6 := mload(0x8a80) + let f_6 := mload(0x9a00) let var121 := mulmod(a_7, var0, r) let var122 := addmod(f_6, var121, r) let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a @@ -1476,8 +1524,8 @@ contract Halo2QuotientEvaluator { let var138 := mulmod(var69, var26, r) let var139 := addmod(var137, var138, r) let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) - let f_7 := mload(0x8aa0) - let a_2_next_1 := mload(0x8600) + let f_7 := mload(0x9a20) + let a_2_next_1 := mload(0x9580) let var141 := mulmod(a_2_next_1, var0, r) let var142 := addmod(f_7, var141, r) let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 @@ -1500,12 +1548,12 @@ contract Halo2QuotientEvaluator { let var160 := mulmod(var69, var30, r) let var161 := addmod(var159, var160, r) let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) - let f_19 := mload(0x8c00) + let f_19 := mload(0x9b80) let q_trash_one_minus_selector := addmod(1, sub(r, f_19), r) - let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x91a0), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0xa120), r) let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) - mstore(0xa300, mulmod(mload(0xa300), y, r)) - mstore(0xa300, addmod(mload(0xa300), q_trash_eval, r)) + mstore(0xb280, mulmod(mload(0xb280), y, r)) + mstore(0xb280, addmod(mload(0xb280), q_trash_eval, r)) } } // Finish selector buckets by applying the codegen-known tail @@ -1517,49 +1565,49 @@ contract Halo2QuotientEvaluator { // selector commitment in the linearized MSM. { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0600)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0600)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x05e0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x05e0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0580)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0580)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x04c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x04c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0460)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0460)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0400)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0400)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x03a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x03a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0340)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0340)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x02e0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x02e0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xa340, 0x0280)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0xb2c0, 0x0280)), r)) } // Fully evaluated identities are the constant-polynomial side // of the linearization query. Rust subtracts that grouped // scalar into expected_eval, so Solidity stores -nu_y(x). - let linearization_expected_eval := addmod(0, sub(r, mload(0xa300)), r) + let linearization_expected_eval := addmod(0, sub(r, mload(0xb280)), r) mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } diff --git a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2Verifier.sol b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2Verifier.sol index 8dd0fcf29..ece93ee6b 100644 --- a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2Verifier.sol +++ b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,34 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. @@ -43,15 +82,15 @@ contract Halo2Verifier { // EXPECTED_VK_PAYLOAD_LENGTH. uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 17024; uint256 internal constant EXPECTED_VK_LENGTH = 17025; - uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x0a489a96da94a3dde90d99d69bd4464d5d5731a89f202154559e68a17fac1c21; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x67bac137fa7e479c25b63324812752e4b6e13d9841d5bf83c322170bf91c0f88; bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); /// @notice Quotient evaluator contract authorized for split quotient reconstruction. /// @dev The evaluator returns the linearization expected scalar and selector buckets; its runtime may be pinned by generated constants. address public immutable AUTHORIZED_QUOTIENT; // Expected split evaluator runtime metadata. It is checked at deployment // and again immediately before each external quotient reconstruction. - uint256 internal constant EXPECTED_QUOTIENT_LENGTH = 9531; - uint256 internal constant EXPECTED_QUOTIENT_CODEHASH_WORD = 0x02f1ea00260a78e72dc51376f5e3053ae6c83deae7219acb89c6a0bca5dc8f1c; + uint256 internal constant EXPECTED_QUOTIENT_LENGTH = 9790; + uint256 internal constant EXPECTED_QUOTIENT_CODEHASH_WORD = 0x7e72c7c5d6fe845370d9431aaa590ab2cd62ca703c3d5b2a862bdb9937195814; bytes32 internal constant EXPECTED_QUOTIENT_CODEHASH = bytes32(EXPECTED_QUOTIENT_CODEHASH_WORD); // Solidity ABI calldata cursors. The generated verifier accepts exactly @@ -63,8 +102,8 @@ contract Halo2Verifier { uint256 internal constant INSTANCE_CPTR = 0x1ee4; // First general-purpose memory words reserved by the generated verifier. // RETURN_MPTR is a single word set to 1 on success. - uint256 internal constant TRANSCRIPT_MPTR = 0x80; - uint256 internal constant RETURN_MPTR = 0x80; + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; // ---------------------------------------------------------------------- // Verifying-key memory map. The VK header lives at VK_MPTR, followed @@ -72,84 +111,87 @@ contract Halo2Verifier { // runtime comes the challenge slots (challenge_mptr..) and the // per-stage scratch (theta_mptr..). // ---------------------------------------------------------------------- - uint256 internal constant VK_MPTR = 0x2700; - uint256 internal constant VK_DIGEST_MPTR = 0x2700; - uint256 internal constant NUM_INSTANCES_MPTR = 0x2720; - uint256 internal constant K_MPTR = 0x2740; - uint256 internal constant N_INV_MPTR = 0x2760; - uint256 internal constant OMEGA_MPTR = 0x2780; - uint256 internal constant OMEGA_INV_MPTR = 0x27a0; - uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x27c0; - uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x27e0; - uint256 internal constant ACC_OFFSET_MPTR = 0x2800; - uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2820; - uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x2840; - uint256 internal constant G1_BASE_MPTR = 0x2860; - uint256 internal constant G2_BASE_MPTR = 0x28e0; - uint256 internal constant NEG_S_G2_BASE_MPTR = 0x29e0; - - uint256 internal constant CHALLENGE_MPTR = 0x6980; + uint256 internal constant VK_MPTR = 0x3680; + uint256 internal constant VK_DIGEST_MPTR = 0x3680; + uint256 internal constant NUM_INSTANCES_MPTR = 0x36a0; + uint256 internal constant K_MPTR = 0x36c0; + uint256 internal constant N_INV_MPTR = 0x36e0; + uint256 internal constant OMEGA_MPTR = 0x3700; + uint256 internal constant OMEGA_INV_MPTR = 0x3720; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x3740; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x3760; + uint256 internal constant ACC_OFFSET_MPTR = 0x3780; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x37a0; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x37c0; + uint256 internal constant G1_BASE_MPTR = 0x37e0; + uint256 internal constant G2_BASE_MPTR = 0x3860; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x3960; + + uint256 internal constant CHALLENGE_MPTR = 0x7900; // Challenge layout. Squeeze order in midnight-proofs: // user_phase challenges (variable count) // theta -> beta, gamma -> trash_challenge -> y -> x -> // x1, x2 -> x3 -> x4 - uint256 internal constant THETA_MPTR = 0x6980; - uint256 internal constant BETA_MPTR = 0x69a0; - uint256 internal constant GAMMA_MPTR = 0x69c0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x69e0; - uint256 internal constant Y_MPTR = 0x6a00; - uint256 internal constant X_MPTR = 0x6a20; - uint256 internal constant X1_MPTR = 0x6a40; - uint256 internal constant X2_MPTR = 0x6a60; - uint256 internal constant X3_MPTR = 0x6a80; - uint256 internal constant X4_MPTR = 0x6aa0; + uint256 internal constant THETA_MPTR = 0x7900; + uint256 internal constant BETA_MPTR = 0x7920; + uint256 internal constant GAMMA_MPTR = 0x7940; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x7960; + uint256 internal constant Y_MPTR = 0x7980; + uint256 internal constant X_MPTR = 0x79a0; + uint256 internal constant X1_MPTR = 0x79c0; + uint256 internal constant X2_MPTR = 0x79e0; + uint256 internal constant X3_MPTR = 0x7a00; + uint256 internal constant X4_MPTR = 0x7a20; // Batch-open commitments live in 4-word EIP-2537 padded slots. - uint256 internal constant F_COM_MPTR = 0x6ac0; - uint256 internal constant PI_MPTR = 0x6b40; + uint256 internal constant F_COM_MPTR = 0x7a40; + uint256 internal constant PI_MPTR = 0x7ac0; // Accumulator (KZG IVC). - uint256 internal constant ACC_LHS_MPTR = 0x6bc0; - uint256 internal constant ACC_RHS_MPTR = 0x6c40; + uint256 internal constant ACC_LHS_MPTR = 0x7b40; + uint256 internal constant ACC_RHS_MPTR = 0x7bc0; // Lagrange / linearization scratch. - uint256 internal constant X_N_MPTR = 0x6cc0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x6ce0; - uint256 internal constant L_LAST_MPTR = 0x6d00; - uint256 internal constant L_BLIND_MPTR = 0x6d20; - uint256 internal constant L_0_MPTR = 0x6d40; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x6d60; + uint256 internal constant X_N_MPTR = 0x7c40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x7c60; + uint256 internal constant L_LAST_MPTR = 0x7c80; + uint256 internal constant L_BLIND_MPTR = 0x7ca0; + uint256 internal constant L_0_MPTR = 0x7cc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x7ce0; // Legacy name: this is not h(x). It stores the expected opening // scalar for the linearized commitment, i.e. the negated y-batched // identity numerator reconstructed from the alleged evals at x. - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x6d80; - uint256 internal constant QUOTIENT_MPTR = 0x6da0; // 4 words - uint256 internal constant F_EVAL_MPTR = 0x6e40; - uint256 internal constant V_MPTR = 0x6e60; - uint256 internal constant FINAL_COM_MPTR = 0x6e80; // 4 words - uint256 internal constant PAIRING_LHS_MPTR = 0x6f00; // 4 words - uint256 internal constant PAIRING_RHS_MPTR = 0x6f80; // 4 words + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x7d00; + uint256 internal constant QUOTIENT_MPTR = 0x7d20; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x7dc0; + uint256 internal constant V_MPTR = 0x7de0; + uint256 internal constant FINAL_COM_MPTR = 0x7e00; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x7e80; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x7f00; // 4 words // Multi-prepare scratch (sized at codegen time). - uint256 internal constant ROT_POINTS_MPTR = 0x7000; - uint256 internal constant X1_POWERS_MPTR = 0x7380; + uint256 internal constant ROT_POINTS_MPTR = 0x7f80; + uint256 internal constant X1_POWERS_MPTR = 0x8300; // Q_COM materialization is currently fused into the final MSM scratch, // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero // reserved capacity until a future emitter starts writing Q_COM_MPTR. - uint256 internal constant Q_COM_MPTR = 0x7ba0; - uint256 internal constant Q_EVAL_SET_MPTR = 0x7ba0; + uint256 internal constant Q_COM_MPTR = 0x8b20; + uint256 internal constant Q_EVAL_SET_MPTR = 0x8b20; // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals // block of the proof; we keep it as a memory slot for symmetry. - uint256 internal constant Q_EVAL_CPTR_MPTR = 0x82a0; + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x9220; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. - uint256 internal constant G1_IDENTITY_MPTR = 0x83a0; + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x9320; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain // Solidity proof shim rewrites proof scalars into canonical BE words, @@ -157,11 +199,15 @@ contract Halo2Verifier { // side `evaluations` loop range-checks and spills that value here so // downstream eval references (gate evaluator + PCS q_eval Horner) // become 3-gas `mload(...)` instead of calldata reads. - uint256 internal constant REVERSED_EVALS_MPTR = 0x8500; - uint256 internal constant SELECTOR_ACC_MPTR = 0xa1c0; - uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; - uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0xa1c0; - uint256 internal constant TRACE_U256_MPTR = 0xd3c0; + uint256 internal constant REVERSED_EVALS_MPTR = 0x9480; + uint256 internal constant SELECTOR_ACC_MPTR = 0xb140; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0xb140; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0xb4e0; + uint256 internal constant TRACE_U256_MPTR = 0xe340; // ---------------------------------------------------------------------- // Per-category bases for EIP-2537 padded G1 commitments. The proof @@ -178,13 +224,67 @@ contract Halo2Verifier { // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans // ---------------------------------------------------------------------- - uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x91c0; - uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x9940; - uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x9a40; - uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x9d40; - uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x9e40; - uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x9f40; - uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x9fc0; + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0xa140; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0xa8c0; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0xa9c0; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0xacc0; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0xadc0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0xaec0; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0xaf40; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 525096; + // Worst-case accumulator RHS MSM: carried RHS point plus every generated + // fixed-base tail scalar nonzero. Zero tail scalars are omitted at + // runtime, which only lowers the actual cost below this bound. + uint256 internal constant ACC_RHS_MSM_GAS = 12000; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x5c2e8be9a8dc4e220b823ce41569f6a757baefeed4df0a04ef5e67db74b36d27; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. @@ -203,10 +303,19 @@ contract Halo2Verifier { /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + // Scratch is reused for every runtime-prerequisite probe. - let scratch := 0x80 + let scratch := 0x1000 // MCOPY must be available because the verifier uses it for // proof-time point/scratch staging. Execute the opcode here so a @@ -224,23 +333,144 @@ contract Halo2Verifier { // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. - let msm_scratch := 0xa1c0 + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0xb140 for { let off := 0 } lt(off, 0x30c0) { off := add(off, 0x20) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x30c0, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x30c0, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -250,7 +480,7 @@ contract Halo2Verifier { // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } @@ -291,24 +521,40 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. function verifyProof( bytes calldata proof, uint256[] calldata instances - ) external view returns (bool) { + ) external returns (bool) { // Cheap ABI-shape guard before any generated memory work: // - proof head must point at the bytes payload; // - instances head must point at the generated instance array. @@ -318,7 +564,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } // Non-embedded renders pin the VK by address and codehash. The Yul @@ -329,24 +578,48 @@ contract Halo2Verifier { // reconstruction to a separately deployed generated evaluator. address quotientEvaluator = AUTHORIZED_QUOTIENT; assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== - // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of // a fixed post-VK address that can collide with live PCS scratch // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } - let p := 0x2600 + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x3580 // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] mstore(add(p, 0x00), 0x20) // base len @@ -355,8 +628,8 @@ contract Halo2Verifier { mstore(add(p, 0x60), x) mstore(add(p, 0x80), sub(FR_MODULUS, 2)) mstore(add(p, 0xa0), FR_MODULUS) - if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -416,16 +689,16 @@ contract Halo2Verifier { let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -481,6 +754,13 @@ contract Halo2Verifier { // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -493,7 +773,7 @@ contract Halo2Verifier { mstore(add(single_scratch, 0x60), x) mstore(add(single_scratch, 0x80), sub(r, 2)) mstore(add(single_scratch, 0xa0), r) - ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) if ret { mstore(mptr_start, mload(single_scratch)) } leave @@ -501,16 +781,34 @@ contract Halo2Verifier { // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -525,8 +823,14 @@ contract Halo2Verifier { mstore(add(gp_mptr, 0x60), gp) mstore(add(gp_mptr, 0x80), sub(r, 2)) mstore(add(gp_mptr, 0xa0), r) - ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -551,22 +855,31 @@ contract Halo2Verifier { // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to // be a 4-step mstore chain for each G1 (~60 gas) and an // 8-iter mstore loop for each G2 (~240 gas). Net saving // here is ~500 gas per ec_pairing call. - let scratch := 0x0300 + let scratch := 0x1240 mcopy(scratch, lhs_mptr, 0x80) mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } @@ -597,7 +910,13 @@ contract Halo2Verifier { // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -765,6 +1084,14 @@ contract Halo2Verifier { // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -800,6 +1127,10 @@ contract Halo2Verifier { // 3. folds the RHS carried point and fixed-base scalar tail into // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for // randomized batching in FinalPairing.yul. + // `r` is consumed only by the canonicality guards in the + // carried-scalar and fixed-base-tail arms; renders whose + // accumulator layout has neither (e.g. point_pair with no tail) + // legally leave it unused. function validate_public_accumulator(success, r) -> out { out := success let bits := 56 @@ -823,11 +1154,16 @@ contract Halo2Verifier { out := and(out, lhs_ok) // Shared scratch for one-pair LHS validation and the later // variable-length RHS MSM. - let acc_scratch := 0xa1c0 + let acc_scratch := 0xb140 { // Carried-scalar layout: the circuit exposes the scalar // that multiplies the carried LHS point. let lhs_scalar := calldataload(lhs_scalar_ptr) + // Canonicality is enforced here rather than relying on the + // later instance-absorption loop: G1MSM reduces scalars + // mod r implicitly, so s and s+r would be indistinguishable + // inside this helper. + out := and(out, lt(lhs_scalar, r)) // Identity status is useful for decoding checks above, but // validation still goes through G1MSM for all points. pop(lhs_is_id) @@ -842,7 +1178,7 @@ contract Halo2Verifier { // Single-pair MSM output overwrites ACC_LHS_MPTR with // lhs_scalar * decoded_lhs. If lhs_scalar is one, this // is also a curve/subgroup validation round-trip. - out := staticcall(gas(), 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) + out := staticcall(G1MSM_GAS_1PAIR, 0x0c, acc_scratch, 0xa0, ACC_LHS_MPTR, 0x80) out := and(out, eq(returndatasize(), 0x80)) } } @@ -862,6 +1198,7 @@ contract Halo2Verifier { { // Explicit carried RHS scalar. let rhs_scalar := calldataload(rhs_scalar_ptr) + out := and(out, lt(rhs_scalar, r)) pop(rhs_is_id) // Keep the carried RHS point in the MSM input even when // it is encoded as identity or has scalar 0/1, so EIP-2537 @@ -887,8 +1224,11 @@ contract Halo2Verifier { // // The precompile also validates every nonzero fixed // base embedded by codegen and the carried RHS point. + // ACC_RHS_MSM_GAS is the compile-time worst case + // (every tail scalar nonzero); acc_msm_len can only + // select a same-size-or-smaller MSM at runtime. out := staticcall( - gas(), + ACC_RHS_MSM_GAS, 0x0c, acc_scratch, acc_msm_len, @@ -903,11 +1243,20 @@ contract Halo2Verifier { } + // Section-boundary gas-attribution checkpoint. Emits a + // single LOG1 (no data) with topic = (id << 248) | gas(). + // Cost: 375 (LOG base) + 375 (1 topic) = 750 gas/call. + // Host-side parses the topic into (id, gas_left) and prints + // pairwise deltas (see `dump_gas_checkpoints`). + function gas_checkpoint(id) { + log1(0, 0, or(shl(248, id), gas())) + } let r := FR_MODULUS let success := true + gas_checkpoint(1) // entry: before VK loading // =============================================================== // VK loading: either bake in the embedded VK bytes or fetch @@ -938,7 +1287,7 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -955,7 +1304,7 @@ contract Halo2Verifier { success := and(success, eq(mload(ACC_OFFSET_MPTR), 4)) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 7)) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 56)) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -979,7 +1328,7 @@ contract Halo2Verifier { ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } // Fail malformed accumulator public inputs before transcript, // quotient, PCS, and final pairing work. The late accumulator block @@ -995,7 +1344,8 @@ contract Halo2Verifier { // success-plumbing style as other helper calls; this boundary is // where the verifier converts failure to a revert. success := validate_public_accumulator(success, r) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_POINT_ENCODING) } + gas_checkpoint(2) // after VK loading + accumulator public-input precheck // =============================================================== // Transcript: VK digest + instances + proof. @@ -1064,8 +1414,9 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } + gas_checkpoint(3) // after VK digest + committed_pi + instance absorbs // =============================================================== // Per-user-phase reads + challenge squeezes. @@ -1103,6 +1454,7 @@ contract Halo2Verifier { advice_walk := add(advice_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(4) // after user-phase advice reads + user challenge squeezes // ---- theta ---- // From this point onward the transcript alternates between @@ -1122,6 +1474,7 @@ contract Halo2Verifier { lookup_m_walk := add(lookup_m_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(5) // after theta squeeze + lookup multiplicities // ---- beta, gamma ---- // beta and gamma are the permutation/lookup randomizers. They are @@ -1141,6 +1494,7 @@ contract Halo2Verifier { perm_z_walk := add(perm_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(6) // after beta/gamma + permutation Z products // ---- lookup helpers + accumulators (per-lookup) ---- // Each lookup contributes zero or more helper commitments followed // by its lookup accumulator Z commitment. The generated layout keeps @@ -1181,6 +1535,7 @@ contract Halo2Verifier { calldatacopy(lookup_z_walk, proof_cptr, 0x80) lookup_z_walk := add(lookup_z_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) + gas_checkpoint(7) // after lookup helpers + Z accumulators // ---- trash_challenge ---- // Midnight squeezes this challenge unconditionally, even when the @@ -1200,6 +1555,7 @@ contract Halo2Verifier { trashcan_walk := add(trashcan_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(8) // after trash_challenge + trashcans // ---- y ---- // y batches all quotient identities. Quotient commitments are read @@ -1223,6 +1579,7 @@ contract Halo2Verifier { quotient_walk := add(quotient_walk, 0x80) proof_cptr := add(proof_cptr, 0x80) } + gas_checkpoint(9) // after y squeeze + quotient-limb reads // ---- x ---- // x is the main evaluation point. Values read after this point are @@ -1247,7 +1604,7 @@ contract Halo2Verifier { // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, 0x20) @@ -1275,6 +1632,16 @@ contract Halo2Verifier { // ---- x3 ---- // x3 is the PCS evaluation point for f_com. buf_len := squeeze_to(buf_len, X3_MPTR) + // truncated-challenges mirrors midnight-proofs + // proofs/src/poly/kzg/mod.rs: + // - x3 is the f_com evaluation point and is truncated + // immediately after squeeze. + // - x1 and x4 remain full squeezed Fr words, but later PCS + // batching stores truncate(x1^i) and truncate(x4^i) while + // keeping the internal power accumulators full precision. + // This direct x3 mask is therefore one part of the PCS truncation + // rule, not the only truncated value used by the verifier. + mstore(X3_MPTR, and(mload(X3_MPTR), 0xffffffffffffffffffffffffffffffff)) // ---- q_evals (one Fq per point set) ---- // q_evals are not spilled into REVERSED_EVALS_MPTR because the PCS @@ -1290,7 +1657,7 @@ contract Halo2Verifier { {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) proof_cptr := add(proof_cptr, 0x20) } @@ -1316,11 +1683,12 @@ contract Halo2Verifier { // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } + gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1339,8 +1707,10 @@ contract Halo2Verifier { // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, 0x0300) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1350,11 +1720,11 @@ contract Halo2Verifier { } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1365,9 +1735,9 @@ contract Halo2Verifier { // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, 0x0140) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0140) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -1390,8 +1760,8 @@ contract Halo2Verifier { // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, 0x0140)) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0140)) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -1400,8 +1770,9 @@ contract Halo2Verifier { mstore(L_0_MPTR, l_0) mstore(INSTANCE_EVAL_MPTR, instance_eval) } + gas_checkpoint(11) // after Lagrange + instance evaluation block - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } // =============================================================== @@ -1423,10 +1794,15 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(quotientEvaluator), EXPECTED_QUOTIENT_LENGTH), eq(extcodehash(quotientEvaluator), EXPECTED_QUOTIENT_CODEHASH_WORD) - )) { revert(0, 0) } - if iszero(staticcall(gas(), quotientEvaluator, 0x2700, 0x6ac0, q_out, 0x0180)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x0180)) { revert(0, 0) } - if iszero(eq(mload(q_out), 0x00000000000000000000000000000000000000000000000051554556414c0001)) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } + // gas() forwarding is deliberate here, unlike the precompile + // call sites: this is a regular contract call, so a reverting + // or failing callee refunds its unused gas -- only precompile + // ERRORS burn everything forwarded (EIP-2537). The callee is + // also pinned by codehash above, not attacker-supplied. + if iszero(staticcall(gas(), quotientEvaluator, 0x3680, 0x6ac0, q_out, 0x0180)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + if iszero(eq(returndatasize(), 0x0180)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + if iszero(eq(mload(q_out), 0x00000000000000000000000000000000000000000000000051554556414c0001)) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } // Word 1 is the negated y-batched identity numerator, stored // in the same memory slot used by the monolithic path. mstore(QUOTIENT_EVAL_MPTR, mload(add(q_out, 0x20))) @@ -1438,6 +1814,7 @@ contract Halo2Verifier { mstore(add(SELECTOR_ACC_MPTR, shl(5, q_i)), mload(add(q_out, add(0x40, shl(5, q_i))))) } } + gas_checkpoint(12) // after batched identity numerator reconstruction // =============================================================== // Prepare linearization scalars for the final PCS MSM. @@ -1479,6 +1856,7 @@ contract Halo2Verifier { mstore(QUOTIENT_MPTR, x_split) mstore(add(QUOTIENT_MPTR, 0x20), one_minus_x_n) } + gas_checkpoint(13) // after linearization scalar prep // =============================================================== // PCS computation (multi-prepare emitter from Step 5). @@ -1517,6 +1895,7 @@ contract Halo2Verifier { x_pow_of_omega := mulmod(x_pow_of_omega, omega_inv, r) mstore(add(ROT_POINTS_MPTR, 0x0), x_pow_of_omega) } + gas_checkpoint(17) // after PCS sub-block 1 // Generated PCS sub-block 2. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -1529,61 +1908,62 @@ contract Halo2Verifier { for { let i := 0 } lt(i, 0x2a) { i := add(i, 1) } { p := add(p, 0x20) acc := mulmod(acc, x1, r) - mstore(p, acc) + mstore(p, and(acc, 0xffffffffffffffffffffffffffffffff)) } } + gas_checkpoint(18) // after PCS sub-block 2 // Generated PCS sub-block 3. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[0]: 43 commitment(s) (rolled, m>=4) + // q_eval_set[0]: 43 evaluation term(s), 42 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0xa300, 0x8a00) - mstore(0xa320, 0x8500) - mstore(0xa340, 0x90a0) - mstore(0xa360, 0x90c0) - mstore(0xa380, 0x9120) - mstore(0xa3a0, 0x9140) - mstore(0xa3c0, 0x91a0) - mstore(0xa3e0, 0x8a20) - mstore(0xa400, 0x8a40) - mstore(0xa420, 0x8a60) - mstore(0xa440, 0x8a80) - mstore(0xa460, 0x8aa0) - mstore(0xa480, 0x8ac0) - mstore(0xa4a0, 0x8ae0) - mstore(0xa4c0, 0x8b00) - mstore(0xa4e0, 0x8b20) - mstore(0xa500, 0x8b40) - mstore(0xa520, 0x8b60) - mstore(0xa540, 0x8b80) - mstore(0xa560, 0x8ba0) - mstore(0xa580, 0x8bc0) - mstore(0xa5a0, 0x8be0) - mstore(0xa5c0, 0x8c00) - mstore(0xa5e0, 0x8c20) - mstore(0xa600, 0x8c40) - mstore(0xa620, 0x8c60) - mstore(0xa640, 0x8c80) - mstore(0xa660, 0x8ca0) - mstore(0xa680, 0x8cc0) - mstore(0xa6a0, 0x8ce0) - mstore(0xa6c0, 0x8d00) - mstore(0xa6e0, 0x8d20) - mstore(0xa700, 0x8d40) - mstore(0xa720, 0x8d60) - mstore(0xa740, 0x8d80) - mstore(0xa760, 0x8da0) - mstore(0xa780, 0x8dc0) - mstore(0xa7a0, 0x8de0) - mstore(0xa7c0, 0x8e00) - mstore(0xa7e0, 0x8e20) - mstore(0xa800, 0x8e40) - mstore(0xa820, 0x8e60) - mstore(0xa840, QUOTIENT_EVAL_MPTR) - let q_eval_set_0 := mload(0x8a00) + mstore(0xb280, 0x9980) + mstore(0xb2a0, 0x9480) + mstore(0xb2c0, 0xa020) + mstore(0xb2e0, 0xa040) + mstore(0xb300, 0xa0a0) + mstore(0xb320, 0xa0c0) + mstore(0xb340, 0xa120) + mstore(0xb360, 0x99a0) + mstore(0xb380, 0x99c0) + mstore(0xb3a0, 0x99e0) + mstore(0xb3c0, 0x9a00) + mstore(0xb3e0, 0x9a20) + mstore(0xb400, 0x9a40) + mstore(0xb420, 0x9a60) + mstore(0xb440, 0x9a80) + mstore(0xb460, 0x9aa0) + mstore(0xb480, 0x9ac0) + mstore(0xb4a0, 0x9ae0) + mstore(0xb4c0, 0x9b00) + mstore(0xb4e0, 0x9b20) + mstore(0xb500, 0x9b40) + mstore(0xb520, 0x9b60) + mstore(0xb540, 0x9b80) + mstore(0xb560, 0x9ba0) + mstore(0xb580, 0x9bc0) + mstore(0xb5a0, 0x9be0) + mstore(0xb5c0, 0x9c00) + mstore(0xb5e0, 0x9c20) + mstore(0xb600, 0x9c40) + mstore(0xb620, 0x9c60) + mstore(0xb640, 0x9c80) + mstore(0xb660, 0x9ca0) + mstore(0xb680, 0x9cc0) + mstore(0xb6a0, 0x9ce0) + mstore(0xb6c0, 0x9d00) + mstore(0xb6e0, 0x9d20) + mstore(0xb700, 0x9d40) + mstore(0xb720, 0x9d60) + mstore(0xb740, 0x9d80) + mstore(0xb760, 0x9da0) + mstore(0xb780, 0x9dc0) + mstore(0xb7a0, 0x9de0) + mstore(0xb7c0, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x9980) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0xa300, 0x20) + let eval_p := add(0xb280, 0x20) for { let i := 1 } lt(i, 0x2b) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -1592,78 +1972,81 @@ contract Halo2Verifier { } mstore(add(Q_EVAL_SET_MPTR, 0x0), q_eval_set_0) } + gas_checkpoint(19) // after PCS sub-block 3 // Generated PCS sub-block 4. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[1]: 3 commitment(s) - let q_eval_set_0 := mload(0x86e0) - let q_eval_set_1 := mload(0x89a0) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x8700), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x89c0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x8720), mload(add(X1_POWERS_MPTR, 0x40)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x89e0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + // q_eval_set[1]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9660) + let q_eval_set_1 := mload(0x9920) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x9680), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9940), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x96a0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9960), mload(add(X1_POWERS_MPTR, 0x40)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } + gas_checkpoint(20) // after PCS sub-block 4 // Generated PCS sub-block 5. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[2]: 3 commitment(s) - let q_eval_set_0 := mload(0x9060) - let q_eval_set_1 := mload(0x9080) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x90e0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9100), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x9160), mload(add(X1_POWERS_MPTR, 0x40)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x9180), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + // q_eval_set[2]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x9fe0) + let q_eval_set_1 := mload(0xa000) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa060), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa080), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0xa0e0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0xa100), mload(add(X1_POWERS_MPTR, 0x40)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) } + gas_checkpoint(21) // after PCS sub-block 5 // Generated PCS sub-block 6. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[3]: 11 commitment(s) (rolled, m>=4) + // q_eval_set[3]: 11 evaluation term(s), 11 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0xa300, 0x8520) - mstore(0xa320, 0x85c0) - mstore(0xa340, 0x8840) - mstore(0xa360, 0x8540) - mstore(0xa380, 0x85e0) - mstore(0xa3a0, 0x8860) - mstore(0xa3c0, 0x8560) - mstore(0xa3e0, 0x8600) - mstore(0xa400, 0x8880) - mstore(0xa420, 0x8580) - mstore(0xa440, 0x8740) - mstore(0xa460, 0x88a0) - mstore(0xa480, 0x85a0) - mstore(0xa4a0, 0x8760) - mstore(0xa4c0, 0x88c0) - mstore(0xa4e0, 0x8620) - mstore(0xa500, 0x8780) - mstore(0xa520, 0x88e0) - mstore(0xa540, 0x8640) - mstore(0xa560, 0x87a0) - mstore(0xa580, 0x8900) - mstore(0xa5a0, 0x8660) - mstore(0xa5c0, 0x87c0) - mstore(0xa5e0, 0x8920) - mstore(0xa600, 0x8680) - mstore(0xa620, 0x87e0) - mstore(0xa640, 0x8940) - mstore(0xa660, 0x86a0) - mstore(0xa680, 0x8800) - mstore(0xa6a0, 0x8960) - mstore(0xa6c0, 0x86c0) - mstore(0xa6e0, 0x8820) - mstore(0xa700, 0x8980) - let q_eval_set_0 := mload(0x8520) - let q_eval_set_1 := mload(0x85c0) - let q_eval_set_2 := mload(0x8840) + mstore(0xb280, 0x94a0) + mstore(0xb2a0, 0x9540) + mstore(0xb2c0, 0x97c0) + mstore(0xb2e0, 0x94c0) + mstore(0xb300, 0x9560) + mstore(0xb320, 0x97e0) + mstore(0xb340, 0x94e0) + mstore(0xb360, 0x9580) + mstore(0xb380, 0x9800) + mstore(0xb3a0, 0x9500) + mstore(0xb3c0, 0x96c0) + mstore(0xb3e0, 0x9820) + mstore(0xb400, 0x9520) + mstore(0xb420, 0x96e0) + mstore(0xb440, 0x9840) + mstore(0xb460, 0x95a0) + mstore(0xb480, 0x9700) + mstore(0xb4a0, 0x9860) + mstore(0xb4c0, 0x95c0) + mstore(0xb4e0, 0x9720) + mstore(0xb500, 0x9880) + mstore(0xb520, 0x95e0) + mstore(0xb540, 0x9740) + mstore(0xb560, 0x98a0) + mstore(0xb580, 0x9600) + mstore(0xb5a0, 0x9760) + mstore(0xb5c0, 0x98c0) + mstore(0xb5e0, 0x9620) + mstore(0xb600, 0x9780) + mstore(0xb620, 0x98e0) + mstore(0xb640, 0x9640) + mstore(0xb660, 0x97a0) + mstore(0xb680, 0x9900) + let q_eval_set_0 := mload(0x94a0) + let q_eval_set_1 := mload(0x9540) + let q_eval_set_2 := mload(0x97c0) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0xa300, 0x60) + let eval_p := add(0xb280, 0x60) for { let i := 1 } lt(i, 0xb) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -1676,32 +2059,33 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_2) } + gas_checkpoint(22) // after PCS sub-block 6 // Generated PCS sub-block 7. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[4]: 5 commitment(s) (rolled, m>=4) + // q_eval_set[4]: 5 evaluation term(s), 5 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0xa300, 0x8e80) - mstore(0xa320, 0x8ea0) - mstore(0xa340, 0x8ec0) - mstore(0xa360, 0x8ee0) - mstore(0xa380, 0x8f00) - mstore(0xa3a0, 0x8f20) - mstore(0xa3c0, 0x8f40) - mstore(0xa3e0, 0x8f60) - mstore(0xa400, 0x8f80) - mstore(0xa420, 0x8fa0) - mstore(0xa440, 0x8fc0) - mstore(0xa460, 0x8fe0) - mstore(0xa480, 0x9000) - mstore(0xa4a0, 0x9020) - mstore(0xa4c0, 0x9040) - let q_eval_set_0 := mload(0x8e80) - let q_eval_set_1 := mload(0x8ea0) - let q_eval_set_2 := mload(0x8ec0) + mstore(0xb280, 0x9e00) + mstore(0xb2a0, 0x9e20) + mstore(0xb2c0, 0x9e40) + mstore(0xb2e0, 0x9e60) + mstore(0xb300, 0x9e80) + mstore(0xb320, 0x9ea0) + mstore(0xb340, 0x9ec0) + mstore(0xb360, 0x9ee0) + mstore(0xb380, 0x9f00) + mstore(0xb3a0, 0x9f20) + mstore(0xb3c0, 0x9f40) + mstore(0xb3e0, 0x9f60) + mstore(0xb400, 0x9f80) + mstore(0xb420, 0x9fa0) + mstore(0xb440, 0x9fc0) + let q_eval_set_0 := mload(0x9e00) + let q_eval_set_1 := mload(0x9e20) + let q_eval_set_2 := mload(0x9e40) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0xa300, 0x60) + let eval_p := add(0xb280, 0x60) for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -1714,6 +2098,7 @@ contract Halo2Verifier { mstore(add(Q_EVAL_SET_MPTR, 0x120), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x140), q_eval_set_2) } + gas_checkpoint(23) // after PCS sub-block 7 // Generated PCS sub-block 8. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -1882,6 +2267,7 @@ contract Halo2Verifier { } mstore(F_EVAL_MPTR, f_eval) } + gas_checkpoint(24) // after PCS sub-block 8 // Generated PCS sub-block 9. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -1892,185 +2278,192 @@ contract Halo2Verifier { let lin_x_split := mload(QUOTIENT_MPTR) let lin_one_minus_x_n := mload(add(QUOTIENT_MPTR, 0x20)) let Q_EVAL_CPTR := mload(Q_EVAL_CPTR_MPTR) - let x4_pow_0 := 1 - let x4_pow_1 := mulmod(x4_pow_0, x4, r) - let x4_pow_2 := mulmod(x4_pow_1, x4, r) - let x4_pow_3 := mulmod(x4_pow_2, x4, r) - let x4_pow_4 := mulmod(x4_pow_3, x4, r) - let x4_pow_5 := mulmod(x4_pow_4, x4, r) + let x4_pow_full := 1 + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_1 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_2 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_3 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_4 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) + x4_pow_full := mulmod(x4_pow_full, x4, r) + let x4_pow_5 := and(x4_pow_full, 0xffffffffffffffffffffffffffffffff) let v := calldataload(Q_EVAL_CPTR) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x80)), x4_pow_4, r), r) v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_5, r), r) - mcopy(0xa300, 0x98c0, 0x80) - mstore(0xa380, 1) - mcopy(0xa3a0, 0x9940, 0x80) - mstore(0xa420, mload(add(X1_POWERS_MPTR, 0x40))) - mcopy(0xa440, 0x9d40, 0x80) - mstore(0xa4c0, mload(add(X1_POWERS_MPTR, 0x60))) - mcopy(0xa4e0, 0x99c0, 0x80) - mstore(0xa560, mload(add(X1_POWERS_MPTR, 0x80))) - mcopy(0xa580, 0x9dc0, 0x80) - mstore(0xa600, mload(add(X1_POWERS_MPTR, 0xa0))) - mcopy(0xa620, 0x9f40, 0x80) - mstore(0xa6a0, mload(add(X1_POWERS_MPTR, 0xc0))) - mcopy(0xa6c0, 0x5780, 0x80) - mstore(0xa740, mload(add(X1_POWERS_MPTR, 0xe0))) - mcopy(0xa760, 0x5500, 0x80) - mstore(0xa7e0, mload(add(X1_POWERS_MPTR, 0x100))) - mcopy(0xa800, 0x5580, 0x80) - mstore(0xa880, mload(add(X1_POWERS_MPTR, 0x120))) - mcopy(0xa8a0, 0x5600, 0x80) - mstore(0xa920, mload(add(X1_POWERS_MPTR, 0x140))) - mcopy(0xa940, 0x5680, 0x80) - mstore(0xa9c0, mload(add(X1_POWERS_MPTR, 0x160))) - mcopy(0xa9e0, 0x5700, 0x80) - mstore(0xaa60, mload(add(X1_POWERS_MPTR, 0x180))) - mcopy(0xaa80, 0x5300, 0x80) - mstore(0xab00, mload(add(X1_POWERS_MPTR, 0x1a0))) - mcopy(0xab20, 0x5380, 0x80) - mstore(0xaba0, mload(add(X1_POWERS_MPTR, 0x1c0))) - mcopy(0xabc0, 0x5400, 0x80) - mstore(0xac40, mload(add(X1_POWERS_MPTR, 0x1e0))) - mcopy(0xac60, 0x5480, 0x80) - mstore(0xace0, mload(add(X1_POWERS_MPTR, 0x200))) - mcopy(0xad00, 0x5800, 0x80) - mstore(0xad80, mload(add(X1_POWERS_MPTR, 0x220))) - mcopy(0xada0, 0x5880, 0x80) - mstore(0xae20, mload(add(X1_POWERS_MPTR, 0x240))) - mcopy(0xae40, 0x5900, 0x80) - mstore(0xaec0, mload(add(X1_POWERS_MPTR, 0x260))) - mcopy(0xaee0, 0x5980, 0x80) - mstore(0xaf60, mload(add(X1_POWERS_MPTR, 0x280))) - mcopy(0xaf80, 0x5b80, 0x80) - mstore(0xb000, mload(add(X1_POWERS_MPTR, 0x2a0))) - mcopy(0xb020, 0x5c80, 0x80) - mstore(0xb0a0, mload(add(X1_POWERS_MPTR, 0x2c0))) - mcopy(0xb0c0, 0x6000, 0x80) - mstore(0xb140, mload(add(X1_POWERS_MPTR, 0x2e0))) - mcopy(0xb160, 0x6080, 0x80) - mstore(0xb1e0, mload(add(X1_POWERS_MPTR, 0x300))) - mcopy(0xb200, 0x6100, 0x80) - mstore(0xb280, mload(add(X1_POWERS_MPTR, 0x320))) - mcopy(0xb2a0, 0x6180, 0x80) - mstore(0xb320, mload(add(X1_POWERS_MPTR, 0x340))) - mcopy(0xb340, 0x6200, 0x80) - mstore(0xb3c0, mload(add(X1_POWERS_MPTR, 0x360))) - mcopy(0xb3e0, 0x6280, 0x80) - mstore(0xb460, mload(add(X1_POWERS_MPTR, 0x380))) - mcopy(0xb480, 0x6300, 0x80) - mstore(0xb500, mload(add(X1_POWERS_MPTR, 0x3a0))) - mcopy(0xb520, 0x6380, 0x80) - mstore(0xb5a0, mload(add(X1_POWERS_MPTR, 0x3c0))) - mcopy(0xb5c0, 0x6400, 0x80) - mstore(0xb640, mload(add(X1_POWERS_MPTR, 0x3e0))) - mcopy(0xb660, 0x6480, 0x80) - mstore(0xb6e0, mload(add(X1_POWERS_MPTR, 0x400))) - mcopy(0xb700, 0x6500, 0x80) - mstore(0xb780, mload(add(X1_POWERS_MPTR, 0x420))) - mcopy(0xb7a0, 0x6580, 0x80) - mstore(0xb820, mload(add(X1_POWERS_MPTR, 0x440))) - mcopy(0xb840, 0x6600, 0x80) - mstore(0xb8c0, mload(add(X1_POWERS_MPTR, 0x460))) - mcopy(0xb8e0, 0x6680, 0x80) - mstore(0xb960, mload(add(X1_POWERS_MPTR, 0x480))) - mcopy(0xb980, 0x6700, 0x80) - mstore(0xba00, mload(add(X1_POWERS_MPTR, 0x4a0))) - mcopy(0xba20, 0x6780, 0x80) - mstore(0xbaa0, mload(add(X1_POWERS_MPTR, 0x4c0))) - mcopy(0xbac0, 0x6800, 0x80) - mstore(0xbb40, mload(add(X1_POWERS_MPTR, 0x4e0))) - mcopy(0xbb60, 0x6880, 0x80) - mstore(0xbbe0, mload(add(X1_POWERS_MPTR, 0x500))) - mcopy(0xbc00, 0x6900, 0x80) - mstore(0xbc80, mload(add(X1_POWERS_MPTR, 0x520))) + mcopy(0xb280, 0xa840, 0x80) + mstore(0xb300, 1) + mcopy(0xb320, 0xa8c0, 0x80) + mstore(0xb3a0, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0xb3c0, 0xacc0, 0x80) + mstore(0xb440, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0xb460, 0xa940, 0x80) + mstore(0xb4e0, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0xb500, 0xad40, 0x80) + mstore(0xb580, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0xb5a0, 0xaec0, 0x80) + mstore(0xb620, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0xb640, 0x6700, 0x80) + mstore(0xb6c0, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0xb6e0, 0x6480, 0x80) + mstore(0xb760, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0xb780, 0x6500, 0x80) + mstore(0xb800, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0xb820, 0x6580, 0x80) + mstore(0xb8a0, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0xb8c0, 0x6600, 0x80) + mstore(0xb940, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0xb960, 0x6680, 0x80) + mstore(0xb9e0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0xba00, 0x6280, 0x80) + mstore(0xba80, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0xbaa0, 0x6300, 0x80) + mstore(0xbb20, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0xbb40, 0x6380, 0x80) + mstore(0xbbc0, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0xbbe0, 0x6400, 0x80) + mstore(0xbc60, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0xbc80, 0x6780, 0x80) + mstore(0xbd00, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0xbd20, 0x6800, 0x80) + mstore(0xbda0, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0xbdc0, 0x6880, 0x80) + mstore(0xbe40, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0xbe60, 0x6900, 0x80) + mstore(0xbee0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0xbf00, 0x6b00, 0x80) + mstore(0xbf80, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0xbfa0, 0x6c00, 0x80) + mstore(0xc020, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0xc040, 0x6f80, 0x80) + mstore(0xc0c0, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0xc0e0, 0x7000, 0x80) + mstore(0xc160, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0xc180, 0x7080, 0x80) + mstore(0xc200, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0xc220, 0x7100, 0x80) + mstore(0xc2a0, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0xc2c0, 0x7180, 0x80) + mstore(0xc340, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0xc360, 0x7200, 0x80) + mstore(0xc3e0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0xc400, 0x7280, 0x80) + mstore(0xc480, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0xc4a0, 0x7300, 0x80) + mstore(0xc520, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0xc540, 0x7380, 0x80) + mstore(0xc5c0, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0xc5e0, 0x7400, 0x80) + mstore(0xc660, mload(add(X1_POWERS_MPTR, 0x400))) + mcopy(0xc680, 0x7480, 0x80) + mstore(0xc700, mload(add(X1_POWERS_MPTR, 0x420))) + mcopy(0xc720, 0x7500, 0x80) + mstore(0xc7a0, mload(add(X1_POWERS_MPTR, 0x440))) + mcopy(0xc7c0, 0x7580, 0x80) + mstore(0xc840, mload(add(X1_POWERS_MPTR, 0x460))) + mcopy(0xc860, 0x7600, 0x80) + mstore(0xc8e0, mload(add(X1_POWERS_MPTR, 0x480))) + mcopy(0xc900, 0x7680, 0x80) + mstore(0xc980, mload(add(X1_POWERS_MPTR, 0x4a0))) + mcopy(0xc9a0, 0x7700, 0x80) + mstore(0xca20, mload(add(X1_POWERS_MPTR, 0x4c0))) + mcopy(0xca40, 0x7780, 0x80) + mstore(0xcac0, mload(add(X1_POWERS_MPTR, 0x4e0))) + mcopy(0xcae0, 0x7800, 0x80) + mstore(0xcb60, mload(add(X1_POWERS_MPTR, 0x500))) + mcopy(0xcb80, 0x7880, 0x80) + mstore(0xcc00, mload(add(X1_POWERS_MPTR, 0x520))) let lin_query_scalar_41 := mload(add(X1_POWERS_MPTR, 0x540)) let lin_cur_scalar_41 := mulmod(lin_query_scalar_41, lin_one_minus_x_n, r) - mcopy(0xbca0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) - mstore(0xbd20, lin_cur_scalar_41) + mcopy(0xcc20, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0xcca0, lin_cur_scalar_41) lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) - mcopy(0xbd40, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) - mstore(0xbdc0, lin_cur_scalar_41) + mcopy(0xccc0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0xcd40, lin_cur_scalar_41) lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) - mcopy(0xbde0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) - mstore(0xbe60, lin_cur_scalar_41) + mcopy(0xcd60, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0xcde0, lin_cur_scalar_41) lin_cur_scalar_41 := mulmod(lin_cur_scalar_41, lin_x_split, r) - mcopy(0xbe80, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) - mstore(0xbf00, lin_cur_scalar_41) - mcopy(0xbf20, 0x5a00, 0x80) - mstore(0xbfa0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) - mcopy(0xbfc0, 0x5a80, 0x80) - mstore(0xc040, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) - mcopy(0xc060, 0x5b00, 0x80) - mstore(0xc0e0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) - mcopy(0xc100, 0x5c00, 0x80) - mstore(0xc180, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) - mcopy(0xc1a0, 0x5d00, 0x80) - mstore(0xc220, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) - mcopy(0xc240, 0x5d80, 0x80) - mstore(0xc2c0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) - mcopy(0xc2e0, 0x5e00, 0x80) - mstore(0xc360, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) - mcopy(0xc380, 0x5e80, 0x80) - mstore(0xc400, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) - mcopy(0xc420, 0x5f00, 0x80) - mstore(0xc4a0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) - mcopy(0xc4c0, 0x5f80, 0x80) - mstore(0xc540, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) - mcopy(0xc560, 0x9740, 0x80) - mstore(0xc5e0, x4_pow_1) - mcopy(0xc600, 0x97c0, 0x80) - mstore(0xc680, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) - mcopy(0xc6a0, 0x9840, 0x80) - mstore(0xc720, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) - mcopy(0xc740, 0x9cc0, 0x80) - mstore(0xc7c0, x4_pow_2) - mcopy(0xc7e0, 0x9e40, 0x80) - mstore(0xc860, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) - mcopy(0xc880, 0x9ec0, 0x80) - mstore(0xc900, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) - mcopy(0xc920, 0x91c0, 0x80) - mstore(0xc9a0, x4_pow_3) - mcopy(0xc9c0, 0x9240, 0x80) - mstore(0xca40, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) - mcopy(0xca60, 0x92c0, 0x80) - mstore(0xcae0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_3, r)) - mcopy(0xcb00, 0x9340, 0x80) - mstore(0xcb80, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_3, r)) - mcopy(0xcba0, 0x93c0, 0x80) - mstore(0xcc20, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_3, r)) - mcopy(0xcc40, 0x9440, 0x80) - mstore(0xccc0, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_3, r)) - mcopy(0xcce0, 0x94c0, 0x80) - mstore(0xcd60, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_3, r)) - mcopy(0xcd80, 0x9540, 0x80) - mstore(0xce00, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_3, r)) - mcopy(0xce20, 0x95c0, 0x80) - mstore(0xcea0, mulmod(mload(add(X1_POWERS_MPTR, 0x100)), x4_pow_3, r)) - mcopy(0xcec0, 0x9640, 0x80) - mstore(0xcf40, mulmod(mload(add(X1_POWERS_MPTR, 0x120)), x4_pow_3, r)) - mcopy(0xcf60, 0x96c0, 0x80) - mstore(0xcfe0, mulmod(mload(add(X1_POWERS_MPTR, 0x140)), x4_pow_3, r)) - mcopy(0xd000, 0x9a40, 0x80) - mstore(0xd080, x4_pow_4) - mcopy(0xd0a0, 0x9ac0, 0x80) - mstore(0xd120, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_4, r)) - mcopy(0xd140, 0x9b40, 0x80) - mstore(0xd1c0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_4, r)) - mcopy(0xd1e0, 0x9bc0, 0x80) - mstore(0xd260, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_4, r)) - mcopy(0xd280, 0x9c40, 0x80) - mstore(0xd300, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_4, r)) - mcopy(0xd320, F_COM_MPTR, 0x80) - mstore(0xd3a0, x4_pow_5) + mcopy(0xce00, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0xce80, lin_cur_scalar_41) + mcopy(0xcea0, 0x6980, 0x80) + mstore(0xcf20, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0xcf40, 0x6a00, 0x80) + mstore(0xcfc0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0xcfe0, 0x6a80, 0x80) + mstore(0xd060, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0xd080, 0x6b80, 0x80) + mstore(0xd100, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0xd120, 0x6c80, 0x80) + mstore(0xd1a0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0xd1c0, 0x6d00, 0x80) + mstore(0xd240, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0xd260, 0x6d80, 0x80) + mstore(0xd2e0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0xd300, 0x6e00, 0x80) + mstore(0xd380, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0xd3a0, 0x6e80, 0x80) + mstore(0xd420, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0xd440, 0x6f00, 0x80) + mstore(0xd4c0, mulmod(lin_query_scalar_41, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0xd4e0, 0xa6c0, 0x80) + mstore(0xd560, x4_pow_1) + mcopy(0xd580, 0xa740, 0x80) + mstore(0xd600, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0xd620, 0xa7c0, 0x80) + mstore(0xd6a0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0xd6c0, 0xac40, 0x80) + mstore(0xd740, x4_pow_2) + mcopy(0xd760, 0xadc0, 0x80) + mstore(0xd7e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0xd800, 0xae40, 0x80) + mstore(0xd880, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0xd8a0, 0xa140, 0x80) + mstore(0xd920, x4_pow_3) + mcopy(0xd940, 0xa1c0, 0x80) + mstore(0xd9c0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xd9e0, 0xa240, 0x80) + mstore(0xda60, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_3, r)) + mcopy(0xda80, 0xa2c0, 0x80) + mstore(0xdb00, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_3, r)) + mcopy(0xdb20, 0xa340, 0x80) + mstore(0xdba0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_3, r)) + mcopy(0xdbc0, 0xa3c0, 0x80) + mstore(0xdc40, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_3, r)) + mcopy(0xdc60, 0xa440, 0x80) + mstore(0xdce0, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_3, r)) + mcopy(0xdd00, 0xa4c0, 0x80) + mstore(0xdd80, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_3, r)) + mcopy(0xdda0, 0xa540, 0x80) + mstore(0xde20, mulmod(mload(add(X1_POWERS_MPTR, 0x100)), x4_pow_3, r)) + mcopy(0xde40, 0xa5c0, 0x80) + mstore(0xdec0, mulmod(mload(add(X1_POWERS_MPTR, 0x120)), x4_pow_3, r)) + mcopy(0xdee0, 0xa640, 0x80) + mstore(0xdf60, mulmod(mload(add(X1_POWERS_MPTR, 0x140)), x4_pow_3, r)) + mcopy(0xdf80, 0xa9c0, 0x80) + mstore(0xe000, x4_pow_4) + mcopy(0xe020, 0xaa40, 0x80) + mstore(0xe0a0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_4, r)) + mcopy(0xe0c0, 0xaac0, 0x80) + mstore(0xe140, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_4, r)) + mcopy(0xe160, 0xab40, 0x80) + mstore(0xe1e0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_4, r)) + mcopy(0xe200, 0xabc0, 0x80) + mstore(0xe280, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_4, r)) + mcopy(0xe2a0, F_COM_MPTR, 0x80) + mstore(0xe320, x4_pow_5) if success { - success := staticcall(gas(), 0x0c, 0xa300, 0x30c0, FINAL_COM_MPTR, 0x80) + // exact EIP-2537 G1MSM cost for 78 pair(s) + success := staticcall(525096, 0x0c, 0xb280, 0x30c0, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) } + gas_checkpoint(25) // after PCS sub-block 9 // Generated PCS sub-block 10. These lines are // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. @@ -2078,30 +2471,31 @@ contract Halo2Verifier { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) - mcopy(0x80, G1_BASE_MPTR, 0x80) - mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, FINAL_COM_MPTR, 0x80) + mcopy(0x1080, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, PI_MPTR, 0x80) - mstore(0x180, mload(X3_MPTR)) + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) if success { - success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) } } + gas_checkpoint(14) // after PCS computation block (= sub-block 6) // Batch the prevalidated public IVC accumulator pairing equation // into the final KZG pairing. @@ -2117,18 +2511,21 @@ contract Halo2Verifier { // If either original equation is bad, this combined equation // holds for at most one alpha in Fr. { - let batch_ptr := 0x0100 + let batch_ptr := 0x1000 - // Domain || KZG rhs/lhs || accumulator rhs/lhs. + // Domain || vk_digest || KZG rhs/lhs || accumulator rhs/lhs. + // vk_digest makes alpha's binding to the verifying key local + // instead of transitive-through-the-points (audit I-7). mstore(batch_ptr, 0x70616972696e672d62617463682d6163632d6b7a670000000000000000) - mcopy(add(batch_ptr, 0x20), PAIRING_RHS_MPTR, 0x80) - mcopy(add(batch_ptr, 0xa0), PAIRING_LHS_MPTR, 0x80) - mcopy(add(batch_ptr, 0x0120), ACC_RHS_MPTR, 0x80) - mcopy(add(batch_ptr, 0x01a0), ACC_LHS_MPTR, 0x80) + mstore(add(batch_ptr, 0x20), mload(VK_DIGEST_MPTR)) + mcopy(add(batch_ptr, 0x40), PAIRING_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0xc0), PAIRING_LHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x0140), ACC_RHS_MPTR, 0x80) + mcopy(add(batch_ptr, 0x01c0), ACC_LHS_MPTR, 0x80) // alpha is Fiat-Shamir over the fully materialized pairing // inputs. Replace the negligible zero draw with one so the // accumulator equation cannot be accidentally dropped. - let acc_pair_alpha := mod(keccak256(batch_ptr, 0x0220), r) + let acc_pair_alpha := mod(keccak256(batch_ptr, 0x0240), r) if iszero(acc_pair_alpha) { acc_pair_alpha := 1 } // PAIRING_RHS_MPTR += alpha * ACC_RHS_MPTR. @@ -2137,12 +2534,12 @@ contract Halo2Verifier { mcopy(batch_ptr, ACC_RHS_MPTR, 0x80) mstore(add(batch_ptr, 0x80), acc_pair_alpha) if success { - success := staticcall(gas(), 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(add(batch_ptr, 0x80), PAIRING_RHS_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_RHS_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } @@ -2151,15 +2548,16 @@ contract Halo2Verifier { mcopy(batch_ptr, ACC_LHS_MPTR, 0x80) mstore(add(batch_ptr, 0x80), acc_pair_alpha) if success { - success := staticcall(gas(), 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, batch_ptr, 0xa0, batch_ptr, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mcopy(add(batch_ptr, 0x80), PAIRING_LHS_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, batch_ptr, 0x0100, PAIRING_LHS_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } } + gas_checkpoint(15) // after public accumulator pairing batch prep (omitted for no-accumulator VKs) // The Yul `ec_pairing` helper checks // e(arg0, G2_BASE) * e(arg1, NEG_S_G2_BASE) == 1 @@ -2174,13 +2572,20 @@ contract Halo2Verifier { // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) + gas_checkpoint(16) // after final ec_pairing // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) } diff --git a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2VerifyingKey.sol b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2VerifyingKey.sol index 7aac985fc..c51a80c36 100644 --- a/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/target/ivc-keccak-solidity-dump/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. @@ -271,149 +274,149 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x19c0), 0x0456a29a706afacf2158850711006fe0acf6a437e9477bf6f782dfac86f2cf7b) // quotient_const mstore(add(payload, 0x19e0), 0x0397cc06bc030aab970dabe70cd498bbeea8daaea65607bb6a872125fec74a10) // quotient_const mstore(add(payload, 0x1a00), 0x73eda753299d7d4833351088b4af7508df8b737010b26e15294bfcbb91950003) // quotient_const - mstore(add(payload, 0x1a20), 0x058560108a8005860008060d000b02000105852011852011852005858008060d) // quotient_program - mstore(add(payload, 0x1a40), 0x000b0300000585401185401185400585a008060d000b03000105856011856011) // quotient_program - mstore(add(payload, 0x1a60), 0x856005862008060d000b0300011b00001b000105860008108b20058520118520) // quotient_program - mstore(add(payload, 0x1a80), 0x1185800d01060585401185401185a00d02060585601185601186200d03060d00) // quotient_program - mstore(add(payload, 0x1aa0), 0x0b0300011b000221020403070002000085200585400685600785800885a00085) // quotient_program - mstore(add(payload, 0x1ac0), 0xc00585e00686000986200a86400b86600c86800d86a00e86c00f86e010870011) // quotient_program - mstore(add(payload, 0x1ae0), 0x87200787400887600987800a87a085200085c00585e006860007874008876009) // quotient_program - mstore(add(payload, 0x1b00), 0x87800a87a085400585c00685e00786000887400987600a87801287a085600685) // quotient_program - mstore(add(payload, 0x1b20), 0xc00785e00886000987400a87601287801387a085800785c00885e00986000a87) // quotient_program - mstore(add(payload, 0x1b40), 0x401287601387801487a085a00885c00985e00a86001287401387601487801587) // quotient_program - mstore(add(payload, 0x1b60), 0xa086200985c00a85e01286001387401487601587801687a086400a85c01285e0) // quotient_program - mstore(add(payload, 0x1b80), 0x1386001487401587601687801787a01887c01988000b04000121021a03070001) // quotient_program - mstore(add(payload, 0x1ba0), 0x000085200585400685601b85801c85a00085c00585e00686001d86201e86400b) // quotient_program - mstore(add(payload, 0x1bc0), 0x86600c86800d86a01f86c02086e02187002287201b87401c87601d87801e87a0) // quotient_program - mstore(add(payload, 0x1be0), 0x85200085c00585e00686001b87401c87601d87801e87a085400585c00685e01b) // quotient_program - mstore(add(payload, 0x1c00), 0x86001c87401d87601e87802387a085600685c01b85e01c86001d87401e876023) // quotient_program - mstore(add(payload, 0x1c20), 0x87802487a085801b85c01c85e01d86001e87402387602487802587a085a01c85) // quotient_program - mstore(add(payload, 0x1c40), 0xc01d85e01e86002387402487602587802687a086201d85c01e85e02386002487) // quotient_program - mstore(add(payload, 0x1c60), 0x402587602687802787a086401e85c02385e02486002587402687602787802887) // quotient_program - mstore(add(payload, 0x1c80), 0xa02987c00b0400011b000321022a01000009000986200a86400b86600c86800d) // quotient_program - mstore(add(payload, 0x1ca0), 0x86a00e86c00f86e00085200585400685600785800885a01087001187201887c0) // quotient_program - mstore(add(payload, 0x1cc0), 0x1988000b05000121022b01000008001d86201e86400b86600c86800d86a01f86) // quotient_program - mstore(add(payload, 0x1ce0), 0xc02086e00085200585400685601b85801c85a02187002287202987c00b050001) // quotient_program - mstore(add(payload, 0x1d00), 0x210388202c0000000b2b0b85200c85400d85602d85c02e85e02f86000b86600c) // quotient_program - mstore(add(payload, 0x1d20), 0x86800d86a03087c03187e00b852086600c852086800d852086a00c854086600d) // quotient_program - mstore(add(payload, 0x1d40), 0x8540868032854087200d856086603285608700338560872032858086e0338580) // quotient_program - mstore(add(payload, 0x1d60), 0x870034858087203285a086c03385a086e03485a087003585a087200085c085c0) // quotient_program - mstore(add(payload, 0x1d80), 0x2e85c085e02f85c086000685e085e03685e087a0368600878037860087a03286) // quotient_program - mstore(add(payload, 0x1da0), 0x2086a033862086c034862086e035862087003886208720328640868033864086) // quotient_program - mstore(add(payload, 0x1dc0), 0xa034864086c035864086e038864087003986408720368740876037874087803a) // quotient_program - mstore(add(payload, 0x1de0), 0x874087a03b876087603a876087803c876087a03d878087803e878087a03f87a0) // quotient_program - mstore(add(payload, 0x1e00), 0x87a00d000b060000210388204003080002150b85200c85400d85600e85800f85) // quotient_program - mstore(add(payload, 0x1e20), 0xa02d85c02e85e02f86001086201186400b86600c86800d86a00e86c00f86e010) // quotient_program - mstore(add(payload, 0x1e40), 0x87001187204187404287604387804487a085200b86600c86800d86a00e86c00f) // quotient_program - mstore(add(payload, 0x1e60), 0x86e010870011872085400c86600d86800e86a00f86c01086e011870045872085) // quotient_program - mstore(add(payload, 0x1e80), 0x600d86600e86800f86a01086c01186e045870046872085800e86600f86801086) // quotient_program - mstore(add(payload, 0x1ea0), 0xa01186c04586e046870047872085a00f86601086801186a04586c04686e04787) // quotient_program - mstore(add(payload, 0x1ec0), 0x0048872085c00085c02e85e02f86004187404287604387804487a08620108660) // quotient_program - mstore(add(payload, 0x1ee0), 0x1186804586a04686c04786e048870049872086401186604586804686a04786c0) // quotient_program - mstore(add(payload, 0x1f00), 0x4886e04987004a87201887c01988000685e085e04185e086004285e087404385) // quotient_program - mstore(add(payload, 0x1f20), 0xe087604485e087804b85e087a00886008600438600874044860087604b860087) // quotient_program - mstore(add(payload, 0x1f40), 0x804c860087a00a874087404b874087604c874087804d874087a013876087604d) // quotient_program - mstore(add(payload, 0x1f60), 0x876087804e876087a015878087804f878087a01787a087a00d000b0600012103) // quotient_program - mstore(add(payload, 0x1f80), 0x88205003080001150b85200c85400d85601f85802085a02d85c02e85e02f8600) // quotient_program - mstore(add(payload, 0x1fa0), 0x2186202286400b86600c86800d86a01f86c02086e02187002287205187405287) // quotient_program - mstore(add(payload, 0x1fc0), 0x605387805487a085200b86600c86800d86a01f86c02086e02187002287208540) // quotient_program - mstore(add(payload, 0x1fe0), 0x0c86600d86801f86a02086c02186e022870055872085600d86601f86802086a0) // quotient_program - mstore(add(payload, 0x2000), 0x2186c02286e055870056872085801f86602086802186a02286c05586e0568700) // quotient_program - mstore(add(payload, 0x2020), 0x57872085a02086602186802286a05586c05686e057870058872085c00085c02e) // quotient_program - mstore(add(payload, 0x2040), 0x85e02f86005187405287605387805487a086202186602286805586a05686c057) // quotient_program - mstore(add(payload, 0x2060), 0x86e058870059872086402286605586805686a05786c05886e05987005a872029) // quotient_program - mstore(add(payload, 0x2080), 0x87c00685e085e05185e086005285e087405385e087605485e087805b85e087a0) // quotient_program - mstore(add(payload, 0x20a0), 0x1c86008600538600874054860087605b860087805c860087a01e874087405b87) // quotient_program - mstore(add(payload, 0x20c0), 0x4087605c874087805d874087a024876087605d876087805e876087a026878087) // quotient_program - mstore(add(payload, 0x20e0), 0x805f878087a02887a087a00d000b06000121038820600000000c390b85200c85) // quotient_program - mstore(add(payload, 0x2100), 0x400d85603087c03187e00088200088400588600688800b89200c89400d896000) // quotient_program - mstore(add(payload, 0x2120), 0x85c088400585c088600685c088800b85c089200c85c089400d85c089600585e0) // quotient_program - mstore(add(payload, 0x2140), 0x88400685e088606185e089000c85e089200d85e089403285e089e00686008840) // quotient_program - mstore(add(payload, 0x2160), 0x61860088e03b860089000d8600892032860089c033860089e000866088200586) // quotient_program - mstore(add(payload, 0x2180), 0x8088200686a0882061874088c03b874088e0628740890032874089a033874089) // quotient_program - mstore(add(payload, 0x21a0), 0xc034874089e061876088a03b876088c062876088e03d87608900328760898033) // quotient_program - mstore(add(payload, 0x21c0), 0x876089a034876089c035876089e061878088803b878088a062878088c03d8780) // quotient_program - mstore(add(payload, 0x21e0), 0x88e063878089003287808960338780898034878089a035878089c038878089e0) // quotient_program - mstore(add(payload, 0x2200), 0x6187a088603b87a088806287a088a03d87a088c06387a088e03f87a089003287) // quotient_program - mstore(add(payload, 0x2220), 0xa089403387a089603487a089803587a089a03887a089c03987a089e00d000b07) // quotient_program - mstore(add(payload, 0x2240), 0x00002103882064020f000a001988000088200088400588600688800788a00888) // quotient_program - mstore(add(payload, 0x2260), 0xc00988e00a89000b89200c89400d89600e89800f89a088200086600586800686) // quotient_program - mstore(add(payload, 0x2280), 0xa00786c00886e00987000a872088400085c00585e00686000787400887600987) // quotient_program - mstore(add(payload, 0x22a0), 0x800a87a088600585c00685e00786000887400987600a87801287a088800685c0) // quotient_program - mstore(add(payload, 0x22c0), 0x0785e00886000987400a87601287801387a088a00785c00885e00986000a8740) // quotient_program - mstore(add(payload, 0x22e0), 0x1287601387801487a088c00885c00985e00a86001287401387601487801587a0) // quotient_program - mstore(add(payload, 0x2300), 0x88e00985c00a85e01286001387401487601587801687a089000a85c01285e013) // quotient_program - mstore(add(payload, 0x2320), 0x86001487401587601687801787a085c00b89200c89400d89600e89800f89a010) // quotient_program - mstore(add(payload, 0x2340), 0x89c01189e085e00c89200d89400e89600f89801089a01189c04589e086000d89) // quotient_program - mstore(add(payload, 0x2360), 0x200e89400f89601089801189a04589c04689e087400e89200f89401089601189) // quotient_program - mstore(add(payload, 0x2380), 0x804589a04689c04789e087600f89201089401189604589804689a04789c04889) // quotient_program - mstore(add(payload, 0x23a0), 0xe087801089201189404589604689804789a04889c04989e087a0118920458940) // quotient_program - mstore(add(payload, 0x23c0), 0x4689604789804889a04989c04a89e00b85200c85400d85600e85800f85a01086) // quotient_program - mstore(add(payload, 0x23e0), 0x201186401887c01089c01189e00d000b0700012103882065020f000900008820) // quotient_program - mstore(add(payload, 0x2400), 0x0088400588600688801b88a01c88c01d88e01e89000b89200c89400d89601f89) // quotient_program - mstore(add(payload, 0x2420), 0x802089a02189c088200086600586800686a01b86c01c86e01d87001e87208840) // quotient_program - mstore(add(payload, 0x2440), 0x0085c00585e00686001b87401c87601d87801e87a088600585c00685e01b8600) // quotient_program - mstore(add(payload, 0x2460), 0x1c87401d87601e87802387a088800685c01b85e01c86001d87401e8760238780) // quotient_program - mstore(add(payload, 0x2480), 0x2487a088a01b85c01c85e01d86001e87402387602487802587a088c01c85c01d) // quotient_program - mstore(add(payload, 0x24a0), 0x85e01e86002387402487602587802687a088e01d85c01e85e023860024874025) // quotient_program - mstore(add(payload, 0x24c0), 0x87602687802787a089001e85c02385e02486002587402687602787802887a085) // quotient_program - mstore(add(payload, 0x24e0), 0xc00b89200c89400d89601f89802089a02189c02289e085e00c89200d89401f89) // quotient_program - mstore(add(payload, 0x2500), 0x602089802189a02289c05589e086000d89201f89402089602189802289a05589) // quotient_program - mstore(add(payload, 0x2520), 0xc05689e087401f89202089402189602289805589a05689c05789e08760208920) // quotient_program - mstore(add(payload, 0x2540), 0x2189402289605589805689a05789c05889e08780218920228940558960568980) // quotient_program - mstore(add(payload, 0x2560), 0x5789a05889c05989e087a02289205589405689605789805889a05989c05a89e0) // quotient_program - mstore(add(payload, 0x2580), 0x0b85200c85400d85601f85802085a02186202286402987c02289e00d000b0700) // quotient_program - mstore(add(payload, 0x25a0), 0x0121038820660000000b2b6785206885406985606a85c06b85e06c86006a8660) // quotient_program - mstore(add(payload, 0x25c0), 0x6b86806c86a03087c03187e06d85208520688520854069852085606e85408540) // quotient_program - mstore(add(payload, 0x25e0), 0x6f854086406f8560862070856086406f858085a0708580862071858086407285) // quotient_program - mstore(add(payload, 0x2600), 0xa085a07185a086207385a086406a85c086606b85c086806c85c086a06b85e086) // quotient_program - mstore(add(payload, 0x2620), 0x606c85e086807485e087206c8600866074860087007586008720768620862077) // quotient_program - mstore(add(payload, 0x2640), 0x86208640788640864074868087a07486a087807586a087a07486c087607586c0) // quotient_program - mstore(add(payload, 0x2660), 0x87807986c087a07486e087407586e087607986e087807a86e087a07587008740) // quotient_program - mstore(add(payload, 0x2680), 0x79870087607a870087807b870087a079872087407a872087607b872087807c87) // quotient_program - mstore(add(payload, 0x26a0), 0x2087a00d000b080000210388207d03080002156785206885406985607e85807f) // quotient_program - mstore(add(payload, 0x26c0), 0x85a06a85c06b85e06c86008086208186406a86606b86806c86a08286c08386e0) // quotient_program - mstore(add(payload, 0x26e0), 0x8487008587208287408387608487808587a085206d85206885406985607e8580) // quotient_program - mstore(add(payload, 0x2700), 0x7f85a080862081864085c06a86606b86806c86a08286c08386e0848700858720) // quotient_program - mstore(add(payload, 0x2720), 0x85e06b86606c86808286a08386c08486e085870086872086006c866082868083) // quotient_program - mstore(add(payload, 0x2740), 0x86a08486c08586e086870087872087408286608386808486a08586c08686e087) // quotient_program - mstore(add(payload, 0x2760), 0x870088872087608386608486808586a08686c08786e088870089872087808486) // quotient_program - mstore(add(payload, 0x2780), 0x608586808686a08786c08886e08987008a872087a08586608686808786a08886) // quotient_program - mstore(add(payload, 0x27a0), 0xc08986e08a87008b87201887c01988006e854085407e854085607f8540858080) // quotient_program - mstore(add(payload, 0x27c0), 0x854085a081854086208c854086408d85608560808560858081856085a08c8560) // quotient_program - mstore(add(payload, 0x27e0), 0x86208e856086408f858085808c858085a08e8580862090858086409185a085a0) // quotient_program - mstore(add(payload, 0x2800), 0x9085a086209285a086409386208620948620864095864086400d000b08000121) // quotient_program - mstore(add(payload, 0x2820), 0x0388209603080001156785206885406985609785809885a06a85c06b85e06c86) // quotient_program - mstore(add(payload, 0x2840), 0x009986209a86406a86606b86806c86a09b86c09c86e09d87009e87209b87409c) // quotient_program - mstore(add(payload, 0x2860), 0x87609d87809e87a085206d85206885406985609785809885a09986209a864085) // quotient_program - mstore(add(payload, 0x2880), 0xc06a86606b86806c86a09b86c09c86e09d87009e872085e06b86606c86809b86) // quotient_program - mstore(add(payload, 0x28a0), 0xa09c86c09d86e09e87009f872086006c86609b86809c86a09d86c09e86e09f87) // quotient_program - mstore(add(payload, 0x28c0), 0x00a0872087409b86609c86809d86a09e86c09f86e0a08700a1872087609c8660) // quotient_program - mstore(add(payload, 0x28e0), 0x9d86809e86a09f86c0a086e0a18700a2872087809d86609e86809f86a0a086c0) // quotient_program - mstore(add(payload, 0x2900), 0xa186e0a28700a3872087a09e86609f8680a086a0a186c0a286e0a38700a48720) // quotient_program - mstore(add(payload, 0x2920), 0x2987c06e854085409785408560988540858099854085a09a85408620a5854086) // quotient_program - mstore(add(payload, 0x2940), 0x40a68560856099856085809a856085a0a585608620a785608640a885808580a5) // quotient_program - mstore(add(payload, 0x2960), 0x858085a0a785808620a985808640aa85a085a0a985a08620ab85a08640ac8620) // quotient_program - mstore(add(payload, 0x2980), 0x8620ad86208640ae864086400d000b08000121038820af0000000e1000852005) // quotient_program - mstore(add(payload, 0x29a0), 0x85400685606a85c06b85e06c86000086600586800686a03087c03187e0008840) // quotient_program - mstore(add(payload, 0x29c0), 0x0588600688800b85c085c06b85c085e06c85c086000d85e085e07485e087a074) // quotient_program - mstore(add(payload, 0x29e0), 0x8600878075860087a07487408760758740878079874087a03387608760798760) // quotient_program - mstore(add(payload, 0x2a00), 0x87807a876087a035878087807b878087a03987a087a00d000b09000021038820) // quotient_program - mstore(add(payload, 0x2a20), 0xb004010002150085200585400685600785800885a06a85c06b85e06c86000986) // quotient_program - mstore(add(payload, 0x2a40), 0x200a86400086600586800686a00786c00886e00987000a872082874083876084) // quotient_program - mstore(add(payload, 0x2a60), 0x87808587a00088400588600688800788a00888c00988e00a890085c00b85c06b) // quotient_program - mstore(add(payload, 0x2a80), 0x85e06c86008287408387608487808587a01887c01988000d85e085e08285e086) // quotient_program - mstore(add(payload, 0x2aa0), 0x008385e087408485e087608585e087808685e087a00f86008600848600874085) // quotient_program - mstore(add(payload, 0x2ac0), 0x86008760868600878087860087a0118740874086874087608787408780888740) // quotient_program - mstore(add(payload, 0x2ae0), 0x87a04687608760888760878089876087a048878087808a878087a04a87a087a0) // quotient_program - mstore(add(payload, 0x2b00), 0x0d000b09000121038820b104010001150085200585400685601b85801c85a06a) // quotient_program - mstore(add(payload, 0x2b20), 0x85c06b85e06c86001d86201e86400086600586800686a01b86c01c86e01d8700) // quotient_program - mstore(add(payload, 0x2b40), 0x1e87209b87409c87609d87809e87a00088400588600688801b88a01c88c01d88) // quotient_program - mstore(add(payload, 0x2b60), 0xe01e890085c00b85c06b85e06c86009b87409c87609d87809e87a02987c00d85) // quotient_program - mstore(add(payload, 0x2b80), 0xe085e09b85e086009c85e087409d85e087609e85e087809f85e087a020860086) // quotient_program - mstore(add(payload, 0x2ba0), 0x009d860087409e860087609f86008780a0860087a022874087409f87408760a0) // quotient_program - mstore(add(payload, 0x2bc0), 0x87408780a1874087a05687608760a187608780a2876087a05887808780a38780) // quotient_program - mstore(add(payload, 0x2be0), 0x87a05a87a087a00d000b090001191f0000000000000000000000000000000000) // quotient_program + mstore(add(payload, 0x1a20), 0x0594e0109a0005958008060d000b0200010594a01194a01194a005950008060d) // quotient_program + mstore(add(payload, 0x1a40), 0x000b0300000594c01194c01194c005952008060d000b0300010594e01194e011) // quotient_program + mstore(add(payload, 0x1a60), 0x94e00595a008060d000b0300011b00001b000105958008109aa00594a01194a0) // quotient_program + mstore(add(payload, 0x1a80), 0x1195000d01060594c01194c01195200d02060594e01194e01195a00d03060d00) // quotient_program + mstore(add(payload, 0x1aa0), 0x0b0300011b000221020403070002000094a00594c00694e00795000895200095) // quotient_program + mstore(add(payload, 0x1ac0), 0x400595600695800995a00a95c00b95e00c96000d96200e96400f966010968011) // quotient_program + mstore(add(payload, 0x1ae0), 0x96a00796c00896e00997000a972094a00095400595600695800796c00896e009) // quotient_program + mstore(add(payload, 0x1b00), 0x97000a972094c00595400695600795800896c00996e00a970012972094e00695) // quotient_program + mstore(add(payload, 0x1b20), 0x400795600895800996c00a96e012970013972095000795400895600995800a96) // quotient_program + mstore(add(payload, 0x1b40), 0xc01296e013970014972095200895400995600a95801296c01396e01497001597) // quotient_program + mstore(add(payload, 0x1b60), 0x2095a00995400a95601295801396c01496e015970016972095c00a9540129560) // quotient_program + mstore(add(payload, 0x1b80), 0x1395801496c01596e01697001797201897401997800b04000121021a03070001) // quotient_program + mstore(add(payload, 0x1ba0), 0x000094a00594c00694e01b95001c95200095400595600695801d95a01e95c00b) // quotient_program + mstore(add(payload, 0x1bc0), 0x95e00c96000d96201f96402096602196802296a01b96c01c96e01d97001e9720) // quotient_program + mstore(add(payload, 0x1be0), 0x94a00095400595600695801b96c01c96e01d97001e972094c00595400695601b) // quotient_program + mstore(add(payload, 0x1c00), 0x95801c96c01d96e01e970023972094e00695401b95601c95801d96c01e96e023) // quotient_program + mstore(add(payload, 0x1c20), 0x970024972095001b95401c95601d95801e96c02396e024970025972095201c95) // quotient_program + mstore(add(payload, 0x1c40), 0x401d95601e95802396c02496e025970026972095a01d95401e95602395802496) // quotient_program + mstore(add(payload, 0x1c60), 0xc02596e026970027972095c01e95402395602495802596c02696e02797002897) // quotient_program + mstore(add(payload, 0x1c80), 0x202997400b0400011b000321022a01000009000995a00a95c00b95e00c96000d) // quotient_program + mstore(add(payload, 0x1ca0), 0x96200e96400f96600094a00594c00694e00795000895201096801196a0189740) // quotient_program + mstore(add(payload, 0x1cc0), 0x1997800b05000121022b01000008001d95a01e95c00b95e00c96000d96201f96) // quotient_program + mstore(add(payload, 0x1ce0), 0x402096600094a00594c00694e01b95001c95202196802296a02997400b050001) // quotient_program + mstore(add(payload, 0x1d00), 0x210397a02c0000000b2b0b94a00c94c00d94e02d95402e95602f95800b95e00c) // quotient_program + mstore(add(payload, 0x1d20), 0x96000d96203097403197600b94a095e00c94a096000d94a096200c94c095e00d) // quotient_program + mstore(add(payload, 0x1d40), 0x94c096003294c096a00d94e095e03294e096803394e096a03295009660339500) // quotient_program + mstore(add(payload, 0x1d60), 0x968034950096a032952096403395209660349520968035952096a00095409540) // quotient_program + mstore(add(payload, 0x1d80), 0x2e954095602f9540958006956095603695609720369580970037958097203295) // quotient_program + mstore(add(payload, 0x1da0), 0xa096203395a096403495a096603595a096803895a096a03295c096003395c096) // quotient_program + mstore(add(payload, 0x1dc0), 0x203495c096403595c096603895c096803995c096a03696c096e03796c097003a) // quotient_program + mstore(add(payload, 0x1de0), 0x96c097203b96e096e03a96e097003c96e097203d970097003e970097203f9720) // quotient_program + mstore(add(payload, 0x1e00), 0x97200d000b060000210397a04003080002150b94a00c94c00d94e00e95000f95) // quotient_program + mstore(add(payload, 0x1e20), 0x202d95402e95602f95801095a01195c00b95e00c96000d96200e96400f966010) // quotient_program + mstore(add(payload, 0x1e40), 0x96801196a04196c04296e043970044972094a00b95e00c96000d96200e96400f) // quotient_program + mstore(add(payload, 0x1e60), 0x96601096801196a094c00c95e00d96000e96200f96401096601196804596a094) // quotient_program + mstore(add(payload, 0x1e80), 0xe00d95e00e96000f96201096401196604596804696a095000e95e00f96001096) // quotient_program + mstore(add(payload, 0x1ea0), 0x201196404596604696804796a095200f95e01096001196204596404696604796) // quotient_program + mstore(add(payload, 0x1ec0), 0x804896a095400095402e95602f95804196c04296e043970044972095a01095e0) // quotient_program + mstore(add(payload, 0x1ee0), 0x1196004596204696404796604896804996a095c01195e0459600469620479640) // quotient_program + mstore(add(payload, 0x1f00), 0x4896604996804a96a01897401997800695609560419560958042956096c04395) // quotient_program + mstore(add(payload, 0x1f20), 0x6096e044956097004b95609720089580958043958096c044958096e04b958097) // quotient_program + mstore(add(payload, 0x1f40), 0x004c958097200a96c096c04b96c096e04c96c097004d96c097201396e096e04d) // quotient_program + mstore(add(payload, 0x1f60), 0x96e097004e96e0972015970097004f9700972017972097200d000b0600012103) // quotient_program + mstore(add(payload, 0x1f80), 0x97a05003080001150b94a00c94c00d94e01f95002095202d95402e95602f9580) // quotient_program + mstore(add(payload, 0x1fa0), 0x2195a02295c00b95e00c96000d96201f96402096602196802296a05196c05296) // quotient_program + mstore(add(payload, 0x1fc0), 0xe053970054972094a00b95e00c96000d96201f96402096602196802296a094c0) // quotient_program + mstore(add(payload, 0x1fe0), 0x0c95e00d96001f96202096402196602296805596a094e00d95e01f9600209620) // quotient_program + mstore(add(payload, 0x2000), 0x2196402296605596805696a095001f95e0209600219620229640559660569680) // quotient_program + mstore(add(payload, 0x2020), 0x5796a095202095e02196002296205596405696605796805896a095400095402e) // quotient_program + mstore(add(payload, 0x2040), 0x95602f95805196c05296e053970054972095a02195e022960055962056964057) // quotient_program + mstore(add(payload, 0x2060), 0x96605896805996a095c02295e05596005696205796405896605996805a96a029) // quotient_program + mstore(add(payload, 0x2080), 0x97400695609560519560958052956096c053956096e054956097005b95609720) // quotient_program + mstore(add(payload, 0x20a0), 0x1c9580958053958096c054958096e05b958097005c958097201e96c096c05b96) // quotient_program + mstore(add(payload, 0x20c0), 0xc096e05c96c097005d96c097202496e096e05d96e097005e96e0972026970097) // quotient_program + mstore(add(payload, 0x20e0), 0x005f9700972028972097200d000b060001210397a0600000000c390b94a00c94) // quotient_program + mstore(add(payload, 0x2100), 0xc00d94e03097403197600097a00097c00597e00698000b98a00c98c00d98e000) // quotient_program + mstore(add(payload, 0x2120), 0x954097c005954097e006954098000b954098a00c954098c00d954098e0059560) // quotient_program + mstore(add(payload, 0x2140), 0x97c006956097e061956098800c956098a00d956098c0329560996006958097c0) // quotient_program + mstore(add(payload, 0x2160), 0x61958098603b958098800d958098a0329580994033958099600095e097a00596) // quotient_program + mstore(add(payload, 0x2180), 0x0097a006962097a06196c098403b96c098606296c098803296c099203396c099) // quotient_program + mstore(add(payload, 0x21a0), 0x403496c099606196e098203b96e098406296e098603d96e098803296e0990033) // quotient_program + mstore(add(payload, 0x21c0), 0x96e099203496e099403596e0996061970098003b9700982062970098403d9700) // quotient_program + mstore(add(payload, 0x21e0), 0x9860639700988032970098e03397009900349700992035970099403897009960) // quotient_program + mstore(add(payload, 0x2200), 0x61972097e03b9720980062972098203d9720984063972098603f972098803297) // quotient_program + mstore(add(payload, 0x2220), 0x2098c033972098e034972099003597209920389720994039972099600d000b07) // quotient_program + mstore(add(payload, 0x2240), 0x0000210397a064020f000a001997800097a00097c00597e00698000798200898) // quotient_program + mstore(add(payload, 0x2260), 0x400998600a98800b98a00c98c00d98e00e99000f992097a00095e00596000696) // quotient_program + mstore(add(payload, 0x2280), 0x200796400896600996800a96a097c00095400595600695800796c00896e00997) // quotient_program + mstore(add(payload, 0x22a0), 0x000a972097e00595400695600795800896c00996e00a97001297209800069540) // quotient_program + mstore(add(payload, 0x22c0), 0x0795600895800996c00a96e012970013972098200795400895600995800a96c0) // quotient_program + mstore(add(payload, 0x22e0), 0x1296e013970014972098400895400995600a95801296c01396e0149700159720) // quotient_program + mstore(add(payload, 0x2300), 0x98600995400a95601295801396c01496e015970016972098800a954012956013) // quotient_program + mstore(add(payload, 0x2320), 0x95801496c01596e016970017972095400b98a00c98c00d98e00e99000f992010) // quotient_program + mstore(add(payload, 0x2340), 0x994011996095600c98a00d98c00e98e00f990010992011994045996095800d98) // quotient_program + mstore(add(payload, 0x2360), 0xa00e98c00f98e010990011992045994046996096c00e98a00f98c01098e01199) // quotient_program + mstore(add(payload, 0x2380), 0x0045992046994047996096e00f98a01098c01198e04599004699204799404899) // quotient_program + mstore(add(payload, 0x23a0), 0x6097001098a01198c04598e046990047992048994049996097201198a04598c0) // quotient_program + mstore(add(payload, 0x23c0), 0x4698e04799004899204999404a99600b94a00c94c00d94e00e95000f95201095) // quotient_program + mstore(add(payload, 0x23e0), 0xa01195c01897401099401199600d000b070001210397a065020f0009000097a0) // quotient_program + mstore(add(payload, 0x2400), 0x0097c00597e00698001b98201c98401d98601e98800b98a00c98c00d98e01f99) // quotient_program + mstore(add(payload, 0x2420), 0x0020992021994097a00095e00596000696201b96401c96601d96801e96a097c0) // quotient_program + mstore(add(payload, 0x2440), 0x0095400595600695801b96c01c96e01d97001e972097e00595400695601b9580) // quotient_program + mstore(add(payload, 0x2460), 0x1c96c01d96e01e970023972098000695401b95601c95801d96c01e96e0239700) // quotient_program + mstore(add(payload, 0x2480), 0x24972098201b95401c95601d95801e96c02396e024970025972098401c95401d) // quotient_program + mstore(add(payload, 0x24a0), 0x95601e95802396c02496e025970026972098601d95401e95602395802496c025) // quotient_program + mstore(add(payload, 0x24c0), 0x96e026970027972098801e95402395602495802596c02696e027970028972095) // quotient_program + mstore(add(payload, 0x24e0), 0x400b98a00c98c00d98e01f990020992021994022996095600c98a00d98c01f98) // quotient_program + mstore(add(payload, 0x2500), 0xe020990021992022994055996095800d98a01f98c02098e02199002299205599) // quotient_program + mstore(add(payload, 0x2520), 0x4056996096c01f98a02098c02198e022990055992056994057996096e02098a0) // quotient_program + mstore(add(payload, 0x2540), 0x2198c02298e055990056992057994058996097002198a02298c05598e0569900) // quotient_program + mstore(add(payload, 0x2560), 0x57992058994059996097202298a05598c05698e05799005899205999405a9960) // quotient_program + mstore(add(payload, 0x2580), 0x0b94a00c94c00d94e01f95002095202195a02295c02997402299600d000b0700) // quotient_program + mstore(add(payload, 0x25a0), 0x01210397a0660000000b2b6794a06894c06994e06a95406b95606c95806a95e0) // quotient_program + mstore(add(payload, 0x25c0), 0x6b96006c96203097403197606d94a094a06894a094c06994a094e06e94c094c0) // quotient_program + mstore(add(payload, 0x25e0), 0x6f94c095c06f94e095a07094e095c06f9500952070950095a071950095c07295) // quotient_program + mstore(add(payload, 0x2600), 0x20952071952095a073952095c06a954095e06b954096006c954096206b956095) // quotient_program + mstore(add(payload, 0x2620), 0xe06c9560960074956096a06c958095e0749580968075958096a07695a095a077) // quotient_program + mstore(add(payload, 0x2640), 0x95a095c07895c095c074960097207496209700759620972074964096e0759640) // quotient_program + mstore(add(payload, 0x2660), 0x9700799640972074966096c075966096e079966097007a9660972075968096c0) // quotient_program + mstore(add(payload, 0x2680), 0x79968096e07a968097007b968097207996a096c07a96a096e07b96a097007c96) // quotient_program + mstore(add(payload, 0x26a0), 0xa097200d000b080000210397a07d03080002156794a06894c06994e07e95007f) // quotient_program + mstore(add(payload, 0x26c0), 0x95206a95406b95606c95808095a08195c06a95e06b96006c9620829640839660) // quotient_program + mstore(add(payload, 0x26e0), 0x8496808596a08296c08396e084970085972094a06d94a06894c06994e07e9500) // quotient_program + mstore(add(payload, 0x2700), 0x7f95208095a08195c095406a95e06b96006c96208296408396608496808596a0) // quotient_program + mstore(add(payload, 0x2720), 0x95606b95e06c96008296208396408496608596808696a095806c95e082960083) // quotient_program + mstore(add(payload, 0x2740), 0x96208496408596608696808796a096c08295e083960084962085964086966087) // quotient_program + mstore(add(payload, 0x2760), 0x96808896a096e08395e08496008596208696408796608896808996a097008495) // quotient_program + mstore(add(payload, 0x2780), 0xe08596008696208796408896608996808a96a097208595e08696008796208896) // quotient_program + mstore(add(payload, 0x27a0), 0x408996608a96808b96a01897401997806e94c094c07e94c094e07f94c0950080) // quotient_program + mstore(add(payload, 0x27c0), 0x94c095208194c095a08c94c095c08d94e094e08094e095008194e095208c94e0) // quotient_program + mstore(add(payload, 0x27e0), 0x95a08e94e095c08f950095008c950095208e950095a090950095c09195209520) // quotient_program + mstore(add(payload, 0x2800), 0x90952095a092952095c09395a095a09495a095c09595c095c00d000b08000121) // quotient_program + mstore(add(payload, 0x2820), 0x0397a09603080001156794a06894c06994e09795009895206a95406b95606c95) // quotient_program + mstore(add(payload, 0x2840), 0x809995a09a95c06a95e06b96006c96209b96409c96609d96809e96a09b96c09c) // quotient_program + mstore(add(payload, 0x2860), 0x96e09d97009e972094a06d94a06894c06994e09795009895209995a09a95c095) // quotient_program + mstore(add(payload, 0x2880), 0x406a95e06b96006c96209b96409c96609d96809e96a095606b95e06c96009b96) // quotient_program + mstore(add(payload, 0x28a0), 0x209c96409d96609e96809f96a095806c95e09b96009c96209d96409e96609f96) // quotient_program + mstore(add(payload, 0x28c0), 0x80a096a096c09b95e09c96009d96209e96409f9660a09680a196a096e09c95e0) // quotient_program + mstore(add(payload, 0x28e0), 0x9d96009e96209f9640a09660a19680a296a097009d95e09e96009f9620a09640) // quotient_program + mstore(add(payload, 0x2900), 0xa19660a29680a396a097209e95e09f9600a09620a19640a29660a39680a496a0) // quotient_program + mstore(add(payload, 0x2920), 0x2997406e94c094c09794c094e09894c095009994c095209a94c095a0a594c095) // quotient_program + mstore(add(payload, 0x2940), 0xc0a694e094e09994e095009a94e09520a594e095a0a794e095c0a895009500a5) // quotient_program + mstore(add(payload, 0x2960), 0x95009520a7950095a0a9950095c0aa95209520a9952095a0ab952095c0ac95a0) // quotient_program + mstore(add(payload, 0x2980), 0x95a0ad95a095c0ae95c095c00d000b080001210397a0af0000000e100094a005) // quotient_program + mstore(add(payload, 0x29a0), 0x94c00694e06a95406b95606c95800095e00596000696203097403197600097c0) // quotient_program + mstore(add(payload, 0x29c0), 0x0597e00698000b954095406b954095606c954095800d95609560749560972074) // quotient_program + mstore(add(payload, 0x29e0), 0x9580970075958097207496c096e07596c097007996c097203396e096e07996e0) // quotient_program + mstore(add(payload, 0x2a00), 0x97007a96e0972035970097007b9700972039972097200d000b090000210397a0) // quotient_program + mstore(add(payload, 0x2a20), 0xb004010002150094a00594c00694e00795000895206a95406b95606c95800995) // quotient_program + mstore(add(payload, 0x2a40), 0xa00a95c00095e00596000696200796400896600996800a96a08296c08396e084) // quotient_program + mstore(add(payload, 0x2a60), 0x97008597200097c00597e00698000798200898400998600a988095400b95406b) // quotient_program + mstore(add(payload, 0x2a80), 0x95606c95808296c08396e08497008597201897401997800d9560956082956095) // quotient_program + mstore(add(payload, 0x2aa0), 0x8083956096c084956096e0859560970086956097200f9580958084958096c085) // quotient_program + mstore(add(payload, 0x2ac0), 0x958096e0869580970087958097201196c096c08696c096e08796c097008896c0) // quotient_program + mstore(add(payload, 0x2ae0), 0x97204696e096e08896e097008996e0972048970097008a970097204a97209720) // quotient_program + mstore(add(payload, 0x2b00), 0x0d000b090001210397a0b104010001150094a00594c00694e01b95001c95206a) // quotient_program + mstore(add(payload, 0x2b20), 0x95406b95606c95801d95a01e95c00095e00596000696201b96401c96601d9680) // quotient_program + mstore(add(payload, 0x2b40), 0x1e96a09b96c09c96e09d97009e97200097c00597e00698001b98201c98401d98) // quotient_program + mstore(add(payload, 0x2b60), 0x601e988095400b95406b95606c95809b96c09c96e09d97009e97202997400d95) // quotient_program + mstore(add(payload, 0x2b80), 0x6095609b956095809c956096c09d956096e09e956097009f9560972020958095) // quotient_program + mstore(add(payload, 0x2ba0), 0x809d958096c09e958096e09f95809700a0958097202296c096c09f96c096e0a0) // quotient_program + mstore(add(payload, 0x2bc0), 0x96c09700a196c097205696e096e0a196e09700a296e097205897009700a39700) // quotient_program + mstore(add(payload, 0x2be0), 0x97205a972097200d000b090001191f0000000000000000000000000000000000) // quotient_program // Fixed-column commitment 0, stored as one // EIP-2537 padded uncompressed G1 slot. mstore(add(payload, 0x2c00), 0x00000000000000000000000000000000055f7961345dce7ce57401dd993cc81a) // fixed_comms[0].x_hi diff --git a/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2Verifier.sol b/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2Verifier.sol index 3660c9092..c44946f7f 100644 --- a/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2Verifier.sol +++ b/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,34 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. @@ -43,7 +82,7 @@ contract Halo2Verifier { // EXPECTED_VK_PAYLOAD_LENGTH. uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 4576; uint256 internal constant EXPECTED_VK_LENGTH = 4577; - uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0xf2c10777bfd922a3700fb18f2e9a2894dfa51dd920f6261bba71ed8b5363e495; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x233a1a8063ef1a9bfd30983d3034d3d90ab75ff6ac95c0e72d770bf08dcff51b; bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); // Solidity ABI calldata cursors. The generated verifier accepts exactly @@ -55,8 +94,8 @@ contract Halo2Verifier { uint256 internal constant INSTANCE_CPTR = 0x1164; // First general-purpose memory words reserved by the generated verifier. // RETURN_MPTR is a single word set to 1 on success. - uint256 internal constant TRANSCRIPT_MPTR = 0x80; - uint256 internal constant RETURN_MPTR = 0x80; + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; // ---------------------------------------------------------------------- // Verifying-key memory map. The VK header lives at VK_MPTR, followed @@ -64,84 +103,87 @@ contract Halo2Verifier { // runtime comes the challenge slots (challenge_mptr..) and the // per-stage scratch (theta_mptr..). // ---------------------------------------------------------------------- - uint256 internal constant VK_MPTR = 0x1980; - uint256 internal constant VK_DIGEST_MPTR = 0x1980; - uint256 internal constant NUM_INSTANCES_MPTR = 0x19a0; - uint256 internal constant K_MPTR = 0x19c0; - uint256 internal constant N_INV_MPTR = 0x19e0; - uint256 internal constant OMEGA_MPTR = 0x1a00; - uint256 internal constant OMEGA_INV_MPTR = 0x1a20; - uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x1a40; - uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x1a60; - uint256 internal constant ACC_OFFSET_MPTR = 0x1a80; - uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x1aa0; - uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x1ac0; - uint256 internal constant G1_BASE_MPTR = 0x1ae0; - uint256 internal constant G2_BASE_MPTR = 0x1b60; - uint256 internal constant NEG_S_G2_BASE_MPTR = 0x1c60; - - uint256 internal constant CHALLENGE_MPTR = 0x2b60; + uint256 internal constant VK_MPTR = 0x2900; + uint256 internal constant VK_DIGEST_MPTR = 0x2900; + uint256 internal constant NUM_INSTANCES_MPTR = 0x2920; + uint256 internal constant K_MPTR = 0x2940; + uint256 internal constant N_INV_MPTR = 0x2960; + uint256 internal constant OMEGA_MPTR = 0x2980; + uint256 internal constant OMEGA_INV_MPTR = 0x29a0; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x29c0; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x29e0; + uint256 internal constant ACC_OFFSET_MPTR = 0x2a00; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2a20; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x2a40; + uint256 internal constant G1_BASE_MPTR = 0x2a60; + uint256 internal constant G2_BASE_MPTR = 0x2ae0; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x2be0; + + uint256 internal constant CHALLENGE_MPTR = 0x3ae0; // Challenge layout. Squeeze order in midnight-proofs: // user_phase challenges (variable count) // theta -> beta, gamma -> trash_challenge -> y -> x -> // x1, x2 -> x3 -> x4 - uint256 internal constant THETA_MPTR = 0x2b60; - uint256 internal constant BETA_MPTR = 0x2b80; - uint256 internal constant GAMMA_MPTR = 0x2ba0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x2bc0; - uint256 internal constant Y_MPTR = 0x2be0; - uint256 internal constant X_MPTR = 0x2c00; - uint256 internal constant X1_MPTR = 0x2c20; - uint256 internal constant X2_MPTR = 0x2c40; - uint256 internal constant X3_MPTR = 0x2c60; - uint256 internal constant X4_MPTR = 0x2c80; + uint256 internal constant THETA_MPTR = 0x3ae0; + uint256 internal constant BETA_MPTR = 0x3b00; + uint256 internal constant GAMMA_MPTR = 0x3b20; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x3b40; + uint256 internal constant Y_MPTR = 0x3b60; + uint256 internal constant X_MPTR = 0x3b80; + uint256 internal constant X1_MPTR = 0x3ba0; + uint256 internal constant X2_MPTR = 0x3bc0; + uint256 internal constant X3_MPTR = 0x3be0; + uint256 internal constant X4_MPTR = 0x3c00; // Batch-open commitments live in 4-word EIP-2537 padded slots. - uint256 internal constant F_COM_MPTR = 0x2ca0; - uint256 internal constant PI_MPTR = 0x2d20; + uint256 internal constant F_COM_MPTR = 0x3c20; + uint256 internal constant PI_MPTR = 0x3ca0; // Accumulator (KZG IVC). - uint256 internal constant ACC_LHS_MPTR = 0x2da0; - uint256 internal constant ACC_RHS_MPTR = 0x2e20; + uint256 internal constant ACC_LHS_MPTR = 0x3d20; + uint256 internal constant ACC_RHS_MPTR = 0x3da0; // Lagrange / linearization scratch. - uint256 internal constant X_N_MPTR = 0x2ea0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x2ec0; - uint256 internal constant L_LAST_MPTR = 0x2ee0; - uint256 internal constant L_BLIND_MPTR = 0x2f00; - uint256 internal constant L_0_MPTR = 0x2f20; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x2f40; + uint256 internal constant X_N_MPTR = 0x3e20; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x3e40; + uint256 internal constant L_LAST_MPTR = 0x3e60; + uint256 internal constant L_BLIND_MPTR = 0x3e80; + uint256 internal constant L_0_MPTR = 0x3ea0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x3ec0; // Legacy name: this is not h(x). It stores the expected opening // scalar for the linearized commitment, i.e. the negated y-batched // identity numerator reconstructed from the alleged evals at x. - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x2f60; - uint256 internal constant QUOTIENT_MPTR = 0x2f80; // 4 words - uint256 internal constant F_EVAL_MPTR = 0x3020; - uint256 internal constant V_MPTR = 0x3040; - uint256 internal constant FINAL_COM_MPTR = 0x3060; // 4 words - uint256 internal constant PAIRING_LHS_MPTR = 0x30e0; // 4 words - uint256 internal constant PAIRING_RHS_MPTR = 0x3160; // 4 words + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x3ee0; + uint256 internal constant QUOTIENT_MPTR = 0x3f00; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x3fa0; + uint256 internal constant V_MPTR = 0x3fc0; + uint256 internal constant FINAL_COM_MPTR = 0x3fe0; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x4060; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x40e0; // 4 words // Multi-prepare scratch (sized at codegen time). - uint256 internal constant ROT_POINTS_MPTR = 0x31e0; - uint256 internal constant X1_POWERS_MPTR = 0x3560; + uint256 internal constant ROT_POINTS_MPTR = 0x4160; + uint256 internal constant X1_POWERS_MPTR = 0x44e0; // Q_COM materialization is currently fused into the final MSM scratch, // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero // reserved capacity until a future emitter starts writing Q_COM_MPTR. - uint256 internal constant Q_COM_MPTR = 0x3d80; - uint256 internal constant Q_EVAL_SET_MPTR = 0x3d80; + uint256 internal constant Q_COM_MPTR = 0x4d00; + uint256 internal constant Q_EVAL_SET_MPTR = 0x4d00; // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals // block of the proof; we keep it as a memory slot for symmetry. - uint256 internal constant Q_EVAL_CPTR_MPTR = 0x4480; + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x5400; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. - uint256 internal constant G1_IDENTITY_MPTR = 0x4580; + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x5500; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain // Solidity proof shim rewrites proof scalars into canonical BE words, @@ -149,11 +191,15 @@ contract Halo2Verifier { // side `evaluations` loop range-checks and spills that value here so // downstream eval references (gate evaluator + PCS q_eval Horner) // become 3-gas `mload(...)` instead of calldata reads. - uint256 internal constant REVERSED_EVALS_MPTR = 0x46e0; - uint256 internal constant SELECTOR_ACC_MPTR = 0x5660; - uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; - uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x5660; - uint256 internal constant TRACE_U256_MPTR = 0x7440; + uint256 internal constant REVERSED_EVALS_MPTR = 0x5660; + uint256 internal constant SELECTOR_ACC_MPTR = 0x65e0; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x65e0; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0x67c0; + uint256 internal constant TRACE_U256_MPTR = 0x83c0; // ---------------------------------------------------------------------- // Per-category bases for EIP-2537 padded G1 commitments. The proof @@ -170,13 +216,63 @@ contract Halo2Verifier { // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans // ---------------------------------------------------------------------- - uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x4ce0; - uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x50e0; - uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x5160; - uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x52e0; - uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x5360; - uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x53e0; - uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x5460; + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x5c60; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x6060; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x60e0; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x6260; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x62e0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x6360; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x63e0; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 337836; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x521a3bd7a16dfa041e1716ee7a4905275d964b4a8a2f9a3d6ee839ea10414812; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. @@ -195,10 +291,19 @@ contract Halo2Verifier { /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + // Scratch is reused for every runtime-prerequisite probe. - let scratch := 0x80 + let scratch := 0x1000 // MCOPY must be available because the verifier uses it for // proof-time point/scratch staging. Execute the opcode here so a @@ -216,23 +321,144 @@ contract Halo2Verifier { // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. - let msm_scratch := 0x5660 + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0x65e0 for { let off := 0 } lt(off, 0x1d60) { off := add(off, 0x20) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x1d60, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x1d60, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -242,7 +468,7 @@ contract Halo2Verifier { // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } @@ -271,17 +497,33 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. @@ -298,7 +540,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } // Non-embedded renders pin the VK by address and codehash. The Yul @@ -306,24 +551,48 @@ contract Halo2Verifier { // INVALID-prefixed payload into VK_MPTR. address vk = AUTHORIZED_VK; assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== - // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of // a fixed post-VK address that can collide with live PCS scratch // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } - let p := 0x1880 + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x2800 // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] mstore(add(p, 0x00), 0x20) // base len @@ -332,8 +601,8 @@ contract Halo2Verifier { mstore(add(p, 0x60), x) mstore(add(p, 0x80), sub(FR_MODULUS, 2)) mstore(add(p, 0xa0), FR_MODULUS) - if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -393,16 +662,16 @@ contract Halo2Verifier { let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -458,6 +727,13 @@ contract Halo2Verifier { // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -470,7 +746,7 @@ contract Halo2Verifier { mstore(add(single_scratch, 0x60), x) mstore(add(single_scratch, 0x80), sub(r, 2)) mstore(add(single_scratch, 0xa0), r) - ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) if ret { mstore(mptr_start, mload(single_scratch)) } leave @@ -478,16 +754,34 @@ contract Halo2Verifier { // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -502,8 +796,14 @@ contract Halo2Verifier { mstore(add(gp_mptr, 0x60), gp) mstore(add(gp_mptr, 0x80), sub(r, 2)) mstore(add(gp_mptr, 0xa0), r) - ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -528,22 +828,31 @@ contract Halo2Verifier { // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to // be a 4-step mstore chain for each G1 (~60 gas) and an // 8-iter mstore loop for each G2 (~240 gas). Net saving // here is ~500 gas per ec_pairing call. - let scratch := 0x0300 + let scratch := 0x1240 mcopy(scratch, lhs_mptr, 0x80) mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } @@ -574,7 +883,13 @@ contract Halo2Verifier { // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -742,6 +1057,14 @@ contract Halo2Verifier { // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -802,7 +1125,7 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -819,7 +1142,7 @@ contract Halo2Verifier { success := and(success, eq(mload(ACC_OFFSET_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 0)) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -843,7 +1166,7 @@ contract Halo2Verifier { ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } // =============================================================== @@ -913,7 +1236,7 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } // =============================================================== @@ -1080,7 +1403,7 @@ contract Halo2Verifier { // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, 0x20) @@ -1133,7 +1456,7 @@ contract Halo2Verifier { {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) proof_cptr := add(proof_cptr, 0x20) } @@ -1159,11 +1482,11 @@ contract Halo2Verifier { // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1182,8 +1505,10 @@ contract Halo2Verifier { // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, 0x0140) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1193,11 +1518,11 @@ contract Halo2Verifier { } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1208,9 +1533,9 @@ contract Halo2Verifier { // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, 0x0120) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0120) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -1233,8 +1558,8 @@ contract Halo2Verifier { // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, 0x0120)) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0120)) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -1244,9 +1569,19 @@ contract Halo2Verifier { mstore(INSTANCE_EVAL_MPTR, instance_eval) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } - // Optional quotient helper functions. Each one is rendered only + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr // helpers and share the same FR_MODULUS as the surrounding @@ -1266,7 +1601,8 @@ contract Halo2Verifier { let q_r := FR_MODULUS let x2 := mulmod(x, x, q_r) z := mulmod(x, mulmod(x2, x2, q_r), q_r) - } // =============================================================== + } + // =============================================================== // Batched identity numerator / linearization target. // // This block does not evaluate the quotient polynomial h(x), and @@ -1362,15 +1698,15 @@ contract Halo2Verifier { // q_const_mptr points to Fr constants used by the VM. // q_program_mptr points to the bytecode stream. // Constants are stored as consecutive 32-byte Fr words. - let q_const_mptr := 0x1d60 + let q_const_mptr := 0x2ce0 // Program bytes are also stored in the VK payload, packed into // 32-byte words by PackedProgramCodec. - let q_program_mptr := 0x1d80 + let q_program_mptr := 0x2d00 // Running Horner accumulator for fully evaluated identities. // After all identities, this is nu_y(x) for the `None` // identity group. // Initialize A = 0 before scanning the identity stream. - mstore(0x56e0, 0) + mstore(0x6660, 0) // Simple selectors are grouped into separate linearization // buckets. They start at zero for every proof. // q_sel_zero_off walks selector bucket byte offsets. @@ -1385,12 +1721,19 @@ contract Halo2Verifier { { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0x66a0, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, 22) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) // Store y^i at selector_power_mptr + 32*i. - mstore(add(0x5720, shl(5, q_y_power_i)), q_y_power) + mstore(add(0x66a0, shl(5, q_y_power_i)), q_y_power) } } @@ -1399,102 +1742,102 @@ contract Halo2Verifier { // VM/native identities, so they occupy the same y-batch order. { let var0 := 0x1 - let f_3 := mload(0x4980) - let f_4 := mload(0x4880) - let a_0 := mload(0x4700) + let f_3 := mload(0x5900) + let f_4 := mload(0x5800) + let a_0 := mload(0x5680) let var1 := mulmod(f_4, a_0, r) let var2 := addmod(f_3, var1, r) - let f_5 := mload(0x48a0) - let a_1 := mload(0x4720) + let f_5 := mload(0x5820) + let a_1 := mload(0x56a0) let var3 := mulmod(f_5, a_1, r) let var4 := addmod(var2, var3, r) - let f_6 := mload(0x48c0) - let a_2 := mload(0x4740) + let f_6 := mload(0x5840) + let a_2 := mload(0x56c0) let var5 := mulmod(f_6, a_2, r) let var6 := addmod(var4, var5, r) - let f_7 := mload(0x48e0) - let a_3 := mload(0x4760) + let f_7 := mload(0x5860) + let a_3 := mload(0x56e0) let var7 := mulmod(f_7, a_3, r) let var8 := addmod(var6, var7, r) - let f_8 := mload(0x4900) - let a_4 := mload(0x4780) + let f_8 := mload(0x5880) + let a_4 := mload(0x5700) let var9 := mulmod(f_8, a_4, r) let var10 := addmod(var8, var9, r) - let f_0 := mload(0x4920) - let a_0_next_1 := mload(0x47a0) + let f_0 := mload(0x58a0) + let a_0_next_1 := mload(0x5720) let var11 := mulmod(f_0, a_0_next_1, r) let var12 := addmod(var10, var11, r) - let f_1 := mload(0x4940) + let f_1 := mload(0x58c0) let var13 := mulmod(f_1, a_0, r) let var14 := mulmod(var13, a_1, r) let var15 := addmod(var12, var14, r) - let f_2 := mload(0x4960) + let f_2 := mload(0x58e0) let var16 := mulmod(f_2, a_0, r) let var17 := mulmod(var16, a_2, r) let var18 := addmod(var15, var17, r) let var19 := mulmod(var0, var18, r) - mstore(0x59e0, var19) + mstore(0x6960, var19) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } { let var0 := 0x1 - let a_1 := mload(0x4720) - let a_2 := mload(0x4740) + let a_1 := mload(0x56a0) + let a_2 := mload(0x56c0) let var1 := addmod(a_1, a_2, r) - let a_3 := mload(0x4760) + let a_3 := mload(0x56e0) let var2 := addmod(0, sub(r, a_3), r) let var3 := addmod(var1, var2, r) - let a_4 := mload(0x4780) + let a_4 := mload(0x5700) let var4 := addmod(0, sub(r, a_4), r) let var5 := addmod(var3, var4, r) let var6 := mulmod(var0, var5, r) - mstore(0x59e0, var6) + mstore(0x6960, var6) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } { let var0 := 0x1 - let a_0 := mload(0x4700) - let f_4 := mload(0x4880) + let a_0 := mload(0x5680) + let f_4 := mload(0x5800) let var1 := addmod(a_0, f_4, r) - let a_0_next_1 := mload(0x47a0) + let a_0_next_1 := mload(0x5720) let var2 := addmod(0, sub(r, a_0_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x59e0, var4) + mstore(0x6960, var4) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } { let var0 := 0x1 - let a_1 := mload(0x4720) - let f_5 := mload(0x48a0) + let a_1 := mload(0x56a0) + let f_5 := mload(0x5820) let var1 := addmod(a_1, f_5, r) - let a_1_next_1 := mload(0x47c0) + let a_1_next_1 := mload(0x5740) let var2 := addmod(0, sub(r, a_1_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x59e0, var4) + mstore(0x6960, var4) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5720, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x66a0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } // VM registers: @@ -1514,24 +1857,26 @@ contract Halo2Verifier { // q_end is an exclusive byte pointer for the VM loop. let q_end := add(q_program_mptr, 0x58) // q_sp starts at the first free stack word. - let q_sp := 0x59e0 + let q_sp := 0x6960 // q_top is meaningless until q_has_top is set. let q_top := 0 // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -1555,6 +1900,7 @@ contract Halo2Verifier { // 64 KiB when this compact form is emitted. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2900), 0x3340) { q_program_fail() } if q_has_top { mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) @@ -1566,6 +1912,7 @@ contract Halo2Verifier { case 0x06 { // The safety validator guarantees a spilled operand // exists before ADD. q_top is the right operand. + if eq(q_sp, 0x6960) { q_program_fail() } q_sp := sub(q_sp, 0x20) q_top := addmod(mload(q_sp), q_top, r) } @@ -1589,6 +1936,7 @@ contract Halo2Verifier { // already range-checked Fr scalar in verifier memory. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2900), 0x3340) { q_program_fail() } q_top := addmod(q_top, mload(q_ptr), r) } // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. @@ -1596,6 +1944,7 @@ contract Halo2Verifier { // In-place multiply by a planned memory word. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2900), 0x3340) { q_program_fail() } q_top := mulmod(q_top, mload(q_ptr), r) } // Native permutation callback. It evaluates the @@ -1614,69 +1963,69 @@ contract Halo2Verifier { // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := 0x59e0 + q_sp := 0x6960 // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. { let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 - let q_perm_vals := 0x59e0 - let q_perm_sigmas := 0x5ae0 - let q_perm_z_cur := 0x5be0 - let q_perm_z_next := 0x5c40 - let q_perm_z_last := 0x5ca0 - let q_perm_delta_base_ptr := 0x5ce0 + let q_perm_vals := 0x6960 + let q_perm_sigmas := 0x6a60 + let q_perm_z_cur := 0x6b60 + let q_perm_z_next := 0x6bc0 + let q_perm_z_last := 0x6c20 + let q_perm_delta_base_ptr := 0x6c60 let q_perm_num_cols := 8 let q_perm_num_sets := 3 let q_perm_chunk_len := 3 let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 - mstore(add(q_perm_vals, 0x0), mload(0x4860)) + mstore(add(q_perm_vals, 0x0), mload(0x57e0)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x4700, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x5680, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0xc0), mload(0x46e0)) + mstore(add(q_perm_vals, 0xc0), mload(0x5660)) mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) { for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 8) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off - mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x4a40, q_perm_sigma_load_src_off))) + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x59c0, q_perm_sigma_load_src_off))) } } { for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 3) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) - mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x4b40, q_perm_z_cur_load_src_off))) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x5ac0, q_perm_z_cur_load_src_off))) } } { for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 3) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) - mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x4b60, q_perm_z_next_load_src_off))) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x5ae0, q_perm_z_next_load_src_off))) } } - mstore(add(q_perm_z_last, 0x0), mload(0x4b80)) - mstore(add(q_perm_z_last, 0x20), mload(0x4be0)) + mstore(add(q_perm_z_last, 0x0), mload(0x5b00)) + mstore(add(q_perm_z_last, 0x20), mload(0x5b60)) let q_perm_eval := 0 q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_perm_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_perm_eval, r)) let q_perm_zn := mload(add(q_perm_z_cur, 0x40)) q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_perm_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_perm_eval, r)) for { let q_perm_i := 1 } lt(q_perm_i, 3) { q_perm_i := add(q_perm_i, 1) } { let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_perm_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_perm_eval, r)) } mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) for { let q_perm_set := 0 } lt(q_perm_set, 3) { q_perm_set := add(q_perm_set, 1) } { @@ -1695,8 +2044,8 @@ contract Halo2Verifier { q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) } q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_perm_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_perm_eval, r)) mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) } } @@ -1717,13 +2066,13 @@ contract Halo2Verifier { // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := 0x59e0 + q_sp := 0x6960 // Generated LogUp code follows the same y-batch order // as the Rust identity stream. { - let q_lookup_f := 0x59e0 - let q_lookup_prefix := 0x5a00 - let q_lookup_suffix := 0x5a20 + let q_lookup_f := 0x6960 + let q_lookup_prefix := 0x6980 + let q_lookup_suffix := 0x69a0 let q_lookup_l0 := mload(L_0_MPTR) let q_lookup_llast := mload(L_LAST_MPTR) let q_lookup_lblind := mload(L_BLIND_MPTR) @@ -1733,33 +2082,33 @@ contract Halo2Verifier { let q_lookup_theta := mload(THETA_MPTR) { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x4c80), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x5c00), r) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_lookup_eval, r)) } { - let f_10 := mload(0x49a0) + let f_10 := mload(0x5920) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) - let a_1 := mload(0x4720) + let a_1 := mload(0x56a0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_1, r) - let q_lookup_eval := addmod(mulmod(mload(0x4c60), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x5be0), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x4c60) - let f_16 := mload(0x4a00) - let f_11 := mload(0x49c0) + let q_lookup_sum_h := mload(0x5be0) + let f_16 := mload(0x5980) + let f_11 := mload(0x5940) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) - let f_12 := mload(0x49e0) + let f_12 := mload(0x5960) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) let q_lookup_s_sum_h := mulmod(f_16, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x4ca0), sub(r, addmod(mload(0x4c80), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x5c20), sub(r, addmod(mload(0x5c00), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x4c40), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x5bc0), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_lookup_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_lookup_eval, r)) } } } @@ -1780,127 +2129,127 @@ contract Halo2Verifier { // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := 0x59e0 + q_sp := 0x6960 // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx case 0 { { let var0 := 0x1 - let f_0 := mload(0x4920) - let a_0_next_1 := mload(0x47a0) + let f_0 := mload(0x58a0) + let a_0_next_1 := mload(0x5720) let var1 := addmod(0, sub(r, a_0_next_1), r) let var2 := addmod(f_0, var1, r) let var3 := 0x1b8114c381b922fd5d6d241210e2d8a68ad5744053ba9e776118de4107b51ace - let a_0 := mload(0x4700) + let a_0 := mload(0x5680) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x4760) + let a_3 := mload(0x56e0) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0x3df32e4cc4cb2ed20e5d21899cf5331775990ccaec4c09b4e3717213fcc0d763 - let a_1 := mload(0x4720) + let a_1 := mload(0x56a0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x4780) + let a_4 := mload(0x5700) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_2 := mload(0x4740) + let a_2 := mload(0x56c0) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x4800) + let a_5 := mload(0x5780) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x59e0, var18) + mstore(0x6960, var18) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5720, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x66a0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } } case 1 { { let var0 := 0x1 - let f_1 := mload(0x4940) - let a_1_next_1 := mload(0x47c0) + let f_1 := mload(0x58c0) + let a_1_next_1 := mload(0x5740) let var1 := addmod(0, sub(r, a_1_next_1), r) let var2 := addmod(f_1, var1, r) let var3 := 0x404d21073985d14e432a4ad76d3fae06ca74314b950fe7b1d7f501cd31a8b374 - let a_0 := mload(0x4700) + let a_0 := mload(0x5680) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x4760) + let a_3 := mload(0x56e0) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0xb2cc8704264c6bd81bc620e9e524d4b73e9b2317679422ff7fa1603955649f1 - let a_1 := mload(0x4720) + let a_1 := mload(0x56a0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x4780) + let a_4 := mload(0x5700) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0xfdf664da55059fa5a9388c641035d496d0bb519834348b4e2a8fc8c637f1a1f - let a_2 := mload(0x4740) + let a_2 := mload(0x56c0) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x4800) + let a_5 := mload(0x5780) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x59e0, var18) + mstore(0x6960, var18) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5720, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x66a0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } } case 2 { { let var0 := 0x1 - let f_2 := mload(0x4960) - let a_2_next_1 := mload(0x47e0) + let f_2 := mload(0x58e0) + let a_2_next_1 := mload(0x5760) let var1 := addmod(0, sub(r, a_2_next_1), r) let var2 := addmod(f_2, var1, r) let var3 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 - let a_0 := mload(0x4700) + let a_0 := mload(0x5680) let var4 := mulmod(a_0, a_0, r) - let a_3 := mload(0x4760) + let a_3 := mload(0x56e0) let var5 := mulmod(var4, a_3, r) let var6 := mulmod(var3, var5, r) let var7 := addmod(var2, var6, r) let var8 := 0x6bd72f9cfc53af9d931896e77ea5c61244cb6d5fae8954f37dc7b9002f5aa78a - let a_1 := mload(0x4720) + let a_1 := mload(0x56a0) let var9 := mulmod(a_1, a_1, r) - let a_4 := mload(0x4780) + let a_4 := mload(0x5700) let var10 := mulmod(var9, a_4, r) let var11 := mulmod(var8, var10, r) let var12 := addmod(var7, var11, r) let var13 := 0x4997c5aa3a5fa07bcaf880a9054bef831effbd9cd58e46d9bb4fb88ef99de0db - let a_2 := mload(0x4740) + let a_2 := mload(0x56c0) let var14 := mulmod(a_2, a_2, r) - let a_5 := mload(0x4800) + let a_5 := mload(0x5780) let var15 := mulmod(var14, a_5, r) let var16 := mulmod(var13, var15, r) let var17 := addmod(var12, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x59e0, var18) + mstore(0x6960, var18) } - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5720, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x59e0), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x66a0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x6960), r)) } } - default { revert(0, 0) } + default { q_program_fail() } } // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. case 0x0b { @@ -1912,6 +2261,11 @@ contract Halo2Verifier { q_pc := add(q_pc, 3) let q_sel_idx := shr(16, q_selector_payload) let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 4)) { q_program_fail() } + if gt(q_sel_gap, 0x15) { q_program_fail() } let q_eval := q_top q_has_top := 0 // Simple-selector identity: keep the same y-batch @@ -1921,28 +2275,34 @@ contract Halo2Verifier { // The global fully-evaluated accumulator is still // multiplied by y so later main identities land at the // same y powers as Rust's reverse fold. - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) let q_sel_acc := mload(q_target_ptr) if q_sel_gap { // Selector buckets are sparse in the global // identity stream. Precomputed y^gap advances only // this selector's local accumulator. - q_sel_acc := mulmod(q_sel_acc, mload(add(0x5720, shl(5, q_sel_gap))), r) + q_sel_acc := mulmod(q_sel_acc, mload(add(0x66a0, shl(5, q_sel_gap))), r) } mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) } // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0x6960)) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -1954,52 +2314,52 @@ contract Halo2Verifier { { let q_trash_tau := mload(TRASH_CHALLENGE_MPTR) { - let f_0 := mload(0x4920) - let a_0_next_1 := mload(0x47a0) + let f_0 := mload(0x58a0) + let a_0_next_1 := mload(0x5720) let var0 := 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000 let var1 := mulmod(a_0_next_1, var0, r) let var2 := addmod(f_0, var1, r) let var3 := 0x590ba402032e82eb1f660ef09796c5686345a5054ed96dae8e2d233633788771 - let a_0 := mload(0x4700) + let a_0 := mload(0x5680) let var4 := mulmod(var3, a_0, r) let var5 := addmod(var2, var4, r) let var6 := 0x52f789e4afc3801f7411102ee2f47cc5954a744e71cac98e75ea962a55a0a76f - let a_1 := mload(0x4720) + let a_1 := mload(0x56a0) let var7 := mulmod(var6, a_1, r) let var8 := addmod(var5, var7, r) let var9 := 0x3509dd2fe3aac0080783557fec090fb1cb4b2b0901253c55282024331d1fe1a8 - let a_2 := mload(0x4740) + let a_2 := mload(0x56c0) let var10 := q_pow5(a_2) let var11 := mulmod(var9, var10, r) let var12 := addmod(var8, var11, r) let var13 := 0x333f8046ece5579cbd6872449c57f2703dfc8864cfadc06d587ff104a0d0c1f2 - let a_3 := mload(0x4760) + let a_3 := mload(0x56e0) let var14 := q_pow5(a_3) let var15 := mulmod(var13, var14, r) let var16 := addmod(var12, var15, r) let var17 := 0x412c98232b6ab8a47aa76ee814ef7ec6261987c9802f2cfc490e007951a60ca5 - let a_4 := mload(0x4780) + let a_4 := mload(0x5700) let var18 := q_pow5(a_4) let var19 := mulmod(var17, var18, r) let var20 := addmod(var16, var19, r) let var21 := 0x53fded36d490ba6b05a5d10fd99ffe5456baec6a6a8753199d5ebdc33c99790e - let a_5 := mload(0x4800) + let a_5 := mload(0x5780) let var22 := q_pow5(a_5) let var23 := mulmod(var21, var22, r) let var24 := addmod(var20, var23, r) let var25 := 0x6ccb1c7d87f3c12a2bde4e68ac7f1e8b03481ba15d7f88f9a7f9b8310dd6d34 - let a_6 := mload(0x4820) + let a_6 := mload(0x57a0) let var26 := q_pow5(a_6) let var27 := mulmod(var25, var26, r) let var28 := addmod(var24, var27, r) let var29 := 0x3f05c4df7a6664dabe258779bf548eb4007f33601591080b3ecd34aea0e1edc1 - let a_7 := mload(0x4840) + let a_7 := mload(0x57c0) let var30 := q_pow5(a_7) let var31 := mulmod(var29, var30, r) let var32 := addmod(var28, var31, r) let var33 := addmod(mulmod(0, q_trash_tau, r), var32, r) - let f_1 := mload(0x4940) - let a_1_next_1 := mload(0x47c0) + let f_1 := mload(0x58c0) + let a_1_next_1 := mload(0x5740) let var34 := mulmod(a_1_next_1, var0, r) let var35 := addmod(f_1, var34, r) let var36 := 0x5b1fc262a28cbb8bf75d9b1a6edaa74591ec24cd9a209512213cec3a3c0f1a5d @@ -2027,7 +2387,7 @@ contract Halo2Verifier { let var58 := mulmod(var57, var30, r) let var59 := addmod(var56, var58, r) let var60 := addmod(mulmod(var33, q_trash_tau, r), var59, r) - let f_2 := mload(0x4960) + let f_2 := mload(0x58e0) let var61 := mulmod(a_3, var0, r) let var62 := addmod(f_2, var61, r) let var63 := 0x5e1d3dbecda6214343e24a47f45c5d033197ad01b65a730af95dc57e90c49140 @@ -2040,7 +2400,7 @@ contract Halo2Verifier { let var70 := mulmod(var69, var10, r) let var71 := addmod(var68, var70, r) let var72 := addmod(mulmod(var60, q_trash_tau, r), var71, r) - let f_3 := mload(0x4980) + let f_3 := mload(0x5900) let var73 := mulmod(a_4, var0, r) let var74 := addmod(f_3, var73, r) let var75 := 0x222e83e70453dfee19b402e9fa8dfe2c4987b034d0be3ceb478b3022e97934c1 @@ -2055,7 +2415,7 @@ contract Halo2Verifier { let var84 := mulmod(var69, var14, r) let var85 := addmod(var83, var84, r) let var86 := addmod(mulmod(var72, q_trash_tau, r), var85, r) - let f_4 := mload(0x4880) + let f_4 := mload(0x5800) let var87 := mulmod(a_5, var0, r) let var88 := addmod(f_4, var87, r) let var89 := 0x726df1506749848155630b86ae25a82b281ecd050fe3a52d85a181fa87202e4b @@ -2072,7 +2432,7 @@ contract Halo2Verifier { let var100 := mulmod(var69, var18, r) let var101 := addmod(var99, var100, r) let var102 := addmod(mulmod(var86, q_trash_tau, r), var101, r) - let f_5 := mload(0x48a0) + let f_5 := mload(0x5820) let var103 := mulmod(a_6, var0, r) let var104 := addmod(f_5, var103, r) let var105 := 0x2f5908b169c6cf1bd26dcf0f9e5105481f5164f3ece0582bf3098312167751a7 @@ -2091,7 +2451,7 @@ contract Halo2Verifier { let var118 := mulmod(var69, var22, r) let var119 := addmod(var117, var118, r) let var120 := addmod(mulmod(var102, q_trash_tau, r), var119, r) - let f_6 := mload(0x48c0) + let f_6 := mload(0x5840) let var121 := mulmod(a_7, var0, r) let var122 := addmod(f_6, var121, r) let var123 := 0x6d05a41959f539a7fc9ec0972ea1e3dbb6fc67dd51daf3414f7fbbb091c7274a @@ -2112,8 +2472,8 @@ contract Halo2Verifier { let var138 := mulmod(var69, var26, r) let var139 := addmod(var137, var138, r) let var140 := addmod(mulmod(var120, q_trash_tau, r), var139, r) - let f_7 := mload(0x48e0) - let a_2_next_1 := mload(0x47e0) + let f_7 := mload(0x5860) + let a_2_next_1 := mload(0x5760) let var141 := mulmod(a_2_next_1, var0, r) let var142 := addmod(f_7, var141, r) let var143 := 0x70d8f2a733a64d650faccc9b1c2a766a9544bb3ff1a11ee73cb43947ef386633 @@ -2136,12 +2496,12 @@ contract Halo2Verifier { let var160 := mulmod(var69, var30, r) let var161 := addmod(var159, var160, r) let var162 := addmod(mulmod(var140, q_trash_tau, r), var161, r) - let f_18 := mload(0x4a20) + let f_18 := mload(0x59a0) let q_trash_one_minus_selector := addmod(1, sub(r, f_18), r) - let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x4cc0), r) + let q_trash_scaled := mulmod(q_trash_one_minus_selector, mload(0x5c40), r) let q_trash_eval := addmod(var162, sub(r, q_trash_scaled), r) - mstore(0x56e0, mulmod(mload(0x56e0), y, r)) - mstore(0x56e0, addmod(mload(0x56e0), q_trash_eval, r)) + mstore(0x6660, mulmod(mload(0x6660), y, r)) + mstore(0x6660, addmod(mload(0x6660), q_trash_eval, r)) } } // Finish selector buckets by applying the codegen-known tail @@ -2153,25 +2513,25 @@ contract Halo2Verifier { // selector commitment in the linearized MSM. { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5720, 0x02a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x66a0, 0x02a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5720, 0x0280)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x66a0, 0x0280)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5720, 0x0220)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x66a0, 0x0220)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5720, 0x0160)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x66a0, 0x0160)), r)) } // Fully evaluated identities are the constant-polynomial side // of the linearization query. Rust subtracts that grouped // scalar into expected_eval, so Solidity stores -nu_y(x). - let linearization_expected_eval := addmod(0, sub(r, mload(0x56e0)), r) + let linearization_expected_eval := addmod(0, sub(r, mload(0x6660)), r) mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } @@ -2271,44 +2631,44 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[0]: 33 commitment(s) (rolled, m>=4) + // q_eval_set[0]: 33 evaluation term(s), 32 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x56e0, 0x4760) - mstore(0x5700, 0x4780) - mstore(0x5720, 0x4800) - mstore(0x5740, 0x4820) - mstore(0x5760, 0x4840) - mstore(0x5780, 0x46e0) - mstore(0x57a0, 0x4c40) - mstore(0x57c0, 0x4c60) - mstore(0x57e0, 0x4cc0) - mstore(0x5800, 0x4860) - mstore(0x5820, 0x4880) - mstore(0x5840, 0x48a0) - mstore(0x5860, 0x48c0) - mstore(0x5880, 0x48e0) - mstore(0x58a0, 0x4900) - mstore(0x58c0, 0x4920) - mstore(0x58e0, 0x4940) - mstore(0x5900, 0x4960) - mstore(0x5920, 0x4980) - mstore(0x5940, 0x49a0) - mstore(0x5960, 0x49c0) - mstore(0x5980, 0x49e0) - mstore(0x59a0, 0x4a00) - mstore(0x59c0, 0x4a20) - mstore(0x59e0, 0x4a40) - mstore(0x5a00, 0x4a60) - mstore(0x5a20, 0x4a80) - mstore(0x5a40, 0x4aa0) - mstore(0x5a60, 0x4ac0) - mstore(0x5a80, 0x4ae0) - mstore(0x5aa0, 0x4b00) - mstore(0x5ac0, 0x4b20) - mstore(0x5ae0, QUOTIENT_EVAL_MPTR) - let q_eval_set_0 := mload(0x4760) + mstore(0x6660, 0x56e0) + mstore(0x6680, 0x5700) + mstore(0x66a0, 0x5780) + mstore(0x66c0, 0x57a0) + mstore(0x66e0, 0x57c0) + mstore(0x6700, 0x5660) + mstore(0x6720, 0x5bc0) + mstore(0x6740, 0x5be0) + mstore(0x6760, 0x5c40) + mstore(0x6780, 0x57e0) + mstore(0x67a0, 0x5800) + mstore(0x67c0, 0x5820) + mstore(0x67e0, 0x5840) + mstore(0x6800, 0x5860) + mstore(0x6820, 0x5880) + mstore(0x6840, 0x58a0) + mstore(0x6860, 0x58c0) + mstore(0x6880, 0x58e0) + mstore(0x68a0, 0x5900) + mstore(0x68c0, 0x5920) + mstore(0x68e0, 0x5940) + mstore(0x6900, 0x5960) + mstore(0x6920, 0x5980) + mstore(0x6940, 0x59a0) + mstore(0x6960, 0x59c0) + mstore(0x6980, 0x59e0) + mstore(0x69a0, 0x5a00) + mstore(0x69c0, 0x5a20) + mstore(0x69e0, 0x5a40) + mstore(0x6a00, 0x5a60) + mstore(0x6a20, 0x5a80) + mstore(0x6a40, 0x5aa0) + mstore(0x6a60, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x56e0) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x56e0, 0x20) + let eval_p := add(0x6660, 0x20) for { let i := 1 } lt(i, 0x21) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2321,22 +2681,22 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[1]: 5 commitment(s) (rolled, m>=4) + // q_eval_set[1]: 5 evaluation term(s), 5 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x56e0, 0x4700) - mstore(0x5700, 0x47a0) - mstore(0x5720, 0x4720) - mstore(0x5740, 0x47c0) - mstore(0x5760, 0x4740) - mstore(0x5780, 0x47e0) - mstore(0x57a0, 0x4c00) - mstore(0x57c0, 0x4c20) - mstore(0x57e0, 0x4c80) - mstore(0x5800, 0x4ca0) - let q_eval_set_0 := mload(0x4700) - let q_eval_set_1 := mload(0x47a0) + mstore(0x6660, 0x5680) + mstore(0x6680, 0x5720) + mstore(0x66a0, 0x56a0) + mstore(0x66c0, 0x5740) + mstore(0x66e0, 0x56c0) + mstore(0x6700, 0x5760) + mstore(0x6720, 0x5b80) + mstore(0x6740, 0x5ba0) + mstore(0x6760, 0x5c00) + mstore(0x6780, 0x5c20) + let q_eval_set_0 := mload(0x5680) + let q_eval_set_1 := mload(0x5720) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x56e0, 0x40) + let eval_p := add(0x6660, 0x40) for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2351,13 +2711,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[2]: 2 commitment(s) - let q_eval_set_0 := mload(0x4b40) - let q_eval_set_1 := mload(0x4b60) - let q_eval_set_2 := mload(0x4b80) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x4ba0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x4bc0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x4be0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + // q_eval_set[2]: 2 evaluation term(s), 2 commitment term(s) + let q_eval_set_0 := mload(0x5ac0) + let q_eval_set_1 := mload(0x5ae0) + let q_eval_set_2 := mload(0x5b00) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x5b20), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x5b40), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x5b60), mload(add(X1_POWERS_MPTR, 0x20)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xa0), q_eval_set_2) @@ -2477,107 +2837,108 @@ contract Halo2Verifier { v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_3, r), r) - mcopy(0x56e0, 0x4e60, 0x80) - mstore(0x5760, 1) - mcopy(0x5780, 0x4ee0, 0x80) - mstore(0x5800, mload(add(X1_POWERS_MPTR, 0x20))) - mcopy(0x5820, 0x4f60, 0x80) - mstore(0x58a0, mload(add(X1_POWERS_MPTR, 0x40))) - mcopy(0x58c0, 0x4fe0, 0x80) - mstore(0x5940, mload(add(X1_POWERS_MPTR, 0x60))) - mcopy(0x5960, 0x5060, 0x80) - mstore(0x59e0, mload(add(X1_POWERS_MPTR, 0x80))) - mcopy(0x5a00, 0x50e0, 0x80) - mstore(0x5a80, mload(add(X1_POWERS_MPTR, 0xc0))) - mcopy(0x5aa0, 0x52e0, 0x80) - mstore(0x5b20, mload(add(X1_POWERS_MPTR, 0xe0))) - mcopy(0x5b40, 0x53e0, 0x80) - mstore(0x5bc0, mload(add(X1_POWERS_MPTR, 0x100))) - mcopy(0x5be0, 0x2260, 0x80) - mstore(0x5c60, mload(add(X1_POWERS_MPTR, 0x120))) - mcopy(0x5c80, 0x1fe0, 0x80) - mstore(0x5d00, mload(add(X1_POWERS_MPTR, 0x140))) - mcopy(0x5d20, 0x2060, 0x80) - mstore(0x5da0, mload(add(X1_POWERS_MPTR, 0x160))) - mcopy(0x5dc0, 0x20e0, 0x80) - mstore(0x5e40, mload(add(X1_POWERS_MPTR, 0x180))) - mcopy(0x5e60, 0x2160, 0x80) - mstore(0x5ee0, mload(add(X1_POWERS_MPTR, 0x1a0))) - mcopy(0x5f00, 0x21e0, 0x80) - mstore(0x5f80, mload(add(X1_POWERS_MPTR, 0x1c0))) - mcopy(0x5fa0, 0x1de0, 0x80) - mstore(0x6020, mload(add(X1_POWERS_MPTR, 0x1e0))) - mcopy(0x6040, 0x1e60, 0x80) - mstore(0x60c0, mload(add(X1_POWERS_MPTR, 0x200))) - mcopy(0x60e0, 0x1ee0, 0x80) - mstore(0x6160, mload(add(X1_POWERS_MPTR, 0x220))) - mcopy(0x6180, 0x1f60, 0x80) - mstore(0x6200, mload(add(X1_POWERS_MPTR, 0x240))) - mcopy(0x6220, 0x22e0, 0x80) - mstore(0x62a0, mload(add(X1_POWERS_MPTR, 0x260))) - mcopy(0x62c0, 0x2360, 0x80) - mstore(0x6340, mload(add(X1_POWERS_MPTR, 0x280))) - mcopy(0x6360, 0x23e0, 0x80) - mstore(0x63e0, mload(add(X1_POWERS_MPTR, 0x2a0))) - mcopy(0x6400, 0x25e0, 0x80) - mstore(0x6480, mload(add(X1_POWERS_MPTR, 0x2c0))) - mcopy(0x64a0, 0x26e0, 0x80) - mstore(0x6520, mload(add(X1_POWERS_MPTR, 0x2e0))) - mcopy(0x6540, 0x2760, 0x80) - mstore(0x65c0, mload(add(X1_POWERS_MPTR, 0x300))) - mcopy(0x65e0, 0x27e0, 0x80) - mstore(0x6660, mload(add(X1_POWERS_MPTR, 0x320))) - mcopy(0x6680, 0x2860, 0x80) - mstore(0x6700, mload(add(X1_POWERS_MPTR, 0x340))) - mcopy(0x6720, 0x28e0, 0x80) - mstore(0x67a0, mload(add(X1_POWERS_MPTR, 0x360))) - mcopy(0x67c0, 0x2960, 0x80) - mstore(0x6840, mload(add(X1_POWERS_MPTR, 0x380))) - mcopy(0x6860, 0x29e0, 0x80) - mstore(0x68e0, mload(add(X1_POWERS_MPTR, 0x3a0))) - mcopy(0x6900, 0x2a60, 0x80) - mstore(0x6980, mload(add(X1_POWERS_MPTR, 0x3c0))) - mcopy(0x69a0, 0x2ae0, 0x80) - mstore(0x6a20, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0x6660, 0x5de0, 0x80) + mstore(0x66e0, 1) + mcopy(0x6700, 0x5e60, 0x80) + mstore(0x6780, mload(add(X1_POWERS_MPTR, 0x20))) + mcopy(0x67a0, 0x5ee0, 0x80) + mstore(0x6820, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0x6840, 0x5f60, 0x80) + mstore(0x68c0, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0x68e0, 0x5fe0, 0x80) + mstore(0x6960, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0x6980, 0x6060, 0x80) + mstore(0x6a00, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0x6a20, 0x6260, 0x80) + mstore(0x6aa0, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0x6ac0, 0x6360, 0x80) + mstore(0x6b40, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0x6b60, 0x31e0, 0x80) + mstore(0x6be0, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0x6c00, 0x2f60, 0x80) + mstore(0x6c80, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0x6ca0, 0x2fe0, 0x80) + mstore(0x6d20, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0x6d40, 0x3060, 0x80) + mstore(0x6dc0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0x6de0, 0x30e0, 0x80) + mstore(0x6e60, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0x6e80, 0x3160, 0x80) + mstore(0x6f00, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0x6f20, 0x2d60, 0x80) + mstore(0x6fa0, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0x6fc0, 0x2de0, 0x80) + mstore(0x7040, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0x7060, 0x2e60, 0x80) + mstore(0x70e0, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0x7100, 0x2ee0, 0x80) + mstore(0x7180, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0x71a0, 0x3260, 0x80) + mstore(0x7220, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0x7240, 0x32e0, 0x80) + mstore(0x72c0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0x72e0, 0x3360, 0x80) + mstore(0x7360, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0x7380, 0x3560, 0x80) + mstore(0x7400, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0x7420, 0x3660, 0x80) + mstore(0x74a0, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0x74c0, 0x36e0, 0x80) + mstore(0x7540, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0x7560, 0x3760, 0x80) + mstore(0x75e0, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0x7600, 0x37e0, 0x80) + mstore(0x7680, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0x76a0, 0x3860, 0x80) + mstore(0x7720, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0x7740, 0x38e0, 0x80) + mstore(0x77c0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0x77e0, 0x3960, 0x80) + mstore(0x7860, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0x7880, 0x39e0, 0x80) + mstore(0x7900, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0x7920, 0x3a60, 0x80) + mstore(0x79a0, mload(add(X1_POWERS_MPTR, 0x3e0))) let lin_query_scalar_31 := mload(add(X1_POWERS_MPTR, 0x400)) let lin_cur_scalar_31 := mulmod(lin_query_scalar_31, lin_one_minus_x_n, r) - mcopy(0x6a40, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) - mstore(0x6ac0, lin_cur_scalar_31) + mcopy(0x79c0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0x7a40, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x6ae0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) - mstore(0x6b60, lin_cur_scalar_31) + mcopy(0x7a60, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0x7ae0, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x6b80, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) - mstore(0x6c00, lin_cur_scalar_31) + mcopy(0x7b00, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0x7b80, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x6c20, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) - mstore(0x6ca0, lin_cur_scalar_31) - mcopy(0x6cc0, 0x2460, 0x80) - mstore(0x6d40, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) - mcopy(0x6d60, 0x24e0, 0x80) - mstore(0x6de0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) - mcopy(0x6e00, 0x2560, 0x80) - mstore(0x6e80, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) - mcopy(0x6ea0, 0x2660, 0x80) - mstore(0x6f20, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) - mcopy(0x6f40, 0x4ce0, 0x80) - mstore(0x6fc0, x4_pow_1) - mcopy(0x6fe0, 0x4d60, 0x80) - mstore(0x7060, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) - mcopy(0x7080, 0x4de0, 0x80) - mstore(0x7100, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) - mcopy(0x7120, 0x5260, 0x80) - mstore(0x71a0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_1, r)) - mcopy(0x71c0, 0x5360, 0x80) - mstore(0x7240, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_1, r)) - mcopy(0x7260, 0x5160, 0x80) - mstore(0x72e0, x4_pow_2) - mcopy(0x7300, 0x51e0, 0x80) - mstore(0x7380, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) - mcopy(0x73a0, F_COM_MPTR, 0x80) - mstore(0x7420, x4_pow_3) + mcopy(0x7ba0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0x7c20, lin_cur_scalar_31) + mcopy(0x7c40, 0x33e0, 0x80) + mstore(0x7cc0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0x7ce0, 0x3460, 0x80) + mstore(0x7d60, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0x7d80, 0x34e0, 0x80) + mstore(0x7e00, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0x7e20, 0x35e0, 0x80) + mstore(0x7ea0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0x7ec0, 0x5c60, 0x80) + mstore(0x7f40, x4_pow_1) + mcopy(0x7f60, 0x5ce0, 0x80) + mstore(0x7fe0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0x8000, 0x5d60, 0x80) + mstore(0x8080, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0x80a0, 0x61e0, 0x80) + mstore(0x8120, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_1, r)) + mcopy(0x8140, 0x62e0, 0x80) + mstore(0x81c0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_1, r)) + mcopy(0x81e0, 0x60e0, 0x80) + mstore(0x8260, x4_pow_2) + mcopy(0x8280, 0x6160, 0x80) + mstore(0x8300, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0x8320, F_COM_MPTR, 0x80) + mstore(0x83a0, x4_pow_3) if success { - success := staticcall(gas(), 0x0c, 0x56e0, 0x1d60, FINAL_COM_MPTR, 0x80) + // exact EIP-2537 G1MSM cost for 47 pair(s) + success := staticcall(337836, 0x0c, 0x6660, 0x1d60, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) @@ -2589,28 +2950,28 @@ contract Halo2Verifier { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) - mcopy(0x80, G1_BASE_MPTR, 0x80) - mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, FINAL_COM_MPTR, 0x80) + mcopy(0x1080, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, PI_MPTR, 0x80) - mstore(0x180, mload(X3_MPTR)) + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) if success { - success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) } } @@ -2641,13 +3002,19 @@ contract Halo2Verifier { // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) } diff --git a/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2VerifyingKey.sol b/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2VerifyingKey.sol index 895f2a4d2..e13e56042 100644 --- a/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/target/poseidon-fixture-dump/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. @@ -94,9 +97,9 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x03a0), 0x00000000000000000000000000000000006d57f79a18220d1e5ef04bd519e995) // neg_s_g2_y_c1_hi mstore(add(payload, 0x03c0), 0x9a9cc71553bb761b5422a6b6971b75c8d3695bfa07b861c4b1c958da426efc45) // neg_s_g2_y_c1_lo mstore(add(payload, 0x03e0), 0x0000000000000000000000000000000000000000000000000000000000000001) // quotient_const - mstore(add(payload, 0x0400), 0x0547401048c00547e008060d000b02000105470011470011470005476008060d) // quotient_program - mstore(add(payload, 0x0420), 0x000b03000005472011472011472005478008060d000b03000105474011474011) // quotient_program - mstore(add(payload, 0x0440), 0x474005480008060d000b0300011b00001b00011b0002191f0000000000000000) // quotient_program + mstore(add(payload, 0x0400), 0x0556c010584005576008060d000b0200010556801156801156800556e008060d) // quotient_program + mstore(add(payload, 0x0420), 0x000b0300000556a01156a01156a005570008060d000b0300010556c01156c011) // quotient_program + mstore(add(payload, 0x0440), 0x56c005578008060d000b0300011b00001b00011b0002191f0000000000000000) // quotient_program // Fixed-column commitment 0, stored as one // EIP-2537 padded uncompressed G1 slot. mstore(add(payload, 0x0460), 0x0000000000000000000000000000000016742a8c4f331d1be5bc8622ba92b271) // fixed_comms[0].x_hi diff --git a/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2Verifier.sol b/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2Verifier.sol index 41513989e..88b985fac 100644 --- a/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2Verifier.sol +++ b/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,34 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. @@ -55,8 +94,8 @@ contract Halo2Verifier { uint256 internal constant INSTANCE_CPTR = 0xec4; // First general-purpose memory words reserved by the generated verifier. // RETURN_MPTR is a single word set to 1 on success. - uint256 internal constant TRANSCRIPT_MPTR = 0x80; - uint256 internal constant RETURN_MPTR = 0x80; + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; // ---------------------------------------------------------------------- // Verifying-key memory map. The VK header lives at VK_MPTR, followed @@ -64,84 +103,87 @@ contract Halo2Verifier { // runtime comes the challenge slots (challenge_mptr..) and the // per-stage scratch (theta_mptr..). // ---------------------------------------------------------------------- - uint256 internal constant VK_MPTR = 0x16e0; - uint256 internal constant VK_DIGEST_MPTR = 0x16e0; - uint256 internal constant NUM_INSTANCES_MPTR = 0x1700; - uint256 internal constant K_MPTR = 0x1720; - uint256 internal constant N_INV_MPTR = 0x1740; - uint256 internal constant OMEGA_MPTR = 0x1760; - uint256 internal constant OMEGA_INV_MPTR = 0x1780; - uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x17a0; - uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x17c0; - uint256 internal constant ACC_OFFSET_MPTR = 0x17e0; - uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x1800; - uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x1820; - uint256 internal constant G1_BASE_MPTR = 0x1840; - uint256 internal constant G2_BASE_MPTR = 0x18c0; - uint256 internal constant NEG_S_G2_BASE_MPTR = 0x19c0; - - uint256 internal constant CHALLENGE_MPTR = 0x2760; + uint256 internal constant VK_MPTR = 0x2660; + uint256 internal constant VK_DIGEST_MPTR = 0x2660; + uint256 internal constant NUM_INSTANCES_MPTR = 0x2680; + uint256 internal constant K_MPTR = 0x26a0; + uint256 internal constant N_INV_MPTR = 0x26c0; + uint256 internal constant OMEGA_MPTR = 0x26e0; + uint256 internal constant OMEGA_INV_MPTR = 0x2700; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x2720; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x2740; + uint256 internal constant ACC_OFFSET_MPTR = 0x2760; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2780; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x27a0; + uint256 internal constant G1_BASE_MPTR = 0x27c0; + uint256 internal constant G2_BASE_MPTR = 0x2840; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x2940; + + uint256 internal constant CHALLENGE_MPTR = 0x36e0; // Challenge layout. Squeeze order in midnight-proofs: // user_phase challenges (variable count) // theta -> beta, gamma -> trash_challenge -> y -> x -> // x1, x2 -> x3 -> x4 - uint256 internal constant THETA_MPTR = 0x2760; - uint256 internal constant BETA_MPTR = 0x2780; - uint256 internal constant GAMMA_MPTR = 0x27a0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x27c0; - uint256 internal constant Y_MPTR = 0x27e0; - uint256 internal constant X_MPTR = 0x2800; - uint256 internal constant X1_MPTR = 0x2820; - uint256 internal constant X2_MPTR = 0x2840; - uint256 internal constant X3_MPTR = 0x2860; - uint256 internal constant X4_MPTR = 0x2880; + uint256 internal constant THETA_MPTR = 0x36e0; + uint256 internal constant BETA_MPTR = 0x3700; + uint256 internal constant GAMMA_MPTR = 0x3720; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x3740; + uint256 internal constant Y_MPTR = 0x3760; + uint256 internal constant X_MPTR = 0x3780; + uint256 internal constant X1_MPTR = 0x37a0; + uint256 internal constant X2_MPTR = 0x37c0; + uint256 internal constant X3_MPTR = 0x37e0; + uint256 internal constant X4_MPTR = 0x3800; // Batch-open commitments live in 4-word EIP-2537 padded slots. - uint256 internal constant F_COM_MPTR = 0x28a0; - uint256 internal constant PI_MPTR = 0x2920; + uint256 internal constant F_COM_MPTR = 0x3820; + uint256 internal constant PI_MPTR = 0x38a0; // Accumulator (KZG IVC). - uint256 internal constant ACC_LHS_MPTR = 0x29a0; - uint256 internal constant ACC_RHS_MPTR = 0x2a20; + uint256 internal constant ACC_LHS_MPTR = 0x3920; + uint256 internal constant ACC_RHS_MPTR = 0x39a0; // Lagrange / linearization scratch. - uint256 internal constant X_N_MPTR = 0x2aa0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x2ac0; - uint256 internal constant L_LAST_MPTR = 0x2ae0; - uint256 internal constant L_BLIND_MPTR = 0x2b00; - uint256 internal constant L_0_MPTR = 0x2b20; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x2b40; + uint256 internal constant X_N_MPTR = 0x3a20; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x3a40; + uint256 internal constant L_LAST_MPTR = 0x3a60; + uint256 internal constant L_BLIND_MPTR = 0x3a80; + uint256 internal constant L_0_MPTR = 0x3aa0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x3ac0; // Legacy name: this is not h(x). It stores the expected opening // scalar for the linearized commitment, i.e. the negated y-batched // identity numerator reconstructed from the alleged evals at x. - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x2b60; - uint256 internal constant QUOTIENT_MPTR = 0x2b80; // 4 words - uint256 internal constant F_EVAL_MPTR = 0x2c20; - uint256 internal constant V_MPTR = 0x2c40; - uint256 internal constant FINAL_COM_MPTR = 0x2c60; // 4 words - uint256 internal constant PAIRING_LHS_MPTR = 0x2ce0; // 4 words - uint256 internal constant PAIRING_RHS_MPTR = 0x2d60; // 4 words + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x3ae0; + uint256 internal constant QUOTIENT_MPTR = 0x3b00; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x3ba0; + uint256 internal constant V_MPTR = 0x3bc0; + uint256 internal constant FINAL_COM_MPTR = 0x3be0; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x3c60; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x3ce0; // 4 words // Multi-prepare scratch (sized at codegen time). - uint256 internal constant ROT_POINTS_MPTR = 0x2de0; - uint256 internal constant X1_POWERS_MPTR = 0x3160; + uint256 internal constant ROT_POINTS_MPTR = 0x3d60; + uint256 internal constant X1_POWERS_MPTR = 0x40e0; // Q_COM materialization is currently fused into the final MSM scratch, // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero // reserved capacity until a future emitter starts writing Q_COM_MPTR. - uint256 internal constant Q_COM_MPTR = 0x3980; - uint256 internal constant Q_EVAL_SET_MPTR = 0x3980; + uint256 internal constant Q_COM_MPTR = 0x4900; + uint256 internal constant Q_EVAL_SET_MPTR = 0x4900; // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals // block of the proof; we keep it as a memory slot for symmetry. - uint256 internal constant Q_EVAL_CPTR_MPTR = 0x4080; + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x5000; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. - uint256 internal constant G1_IDENTITY_MPTR = 0x4180; + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x5100; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain // Solidity proof shim rewrites proof scalars into canonical BE words, @@ -149,11 +191,15 @@ contract Halo2Verifier { // side `evaluations` loop range-checks and spills that value here so // downstream eval references (gate evaluator + PCS q_eval Horner) // become 3-gas `mload(...)` instead of calldata reads. - uint256 internal constant REVERSED_EVALS_MPTR = 0x42e0; - uint256 internal constant SELECTOR_ACC_MPTR = 0x4fc0; - uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; - uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x4fc0; - uint256 internal constant TRACE_U256_MPTR = 0x7000; + uint256 internal constant REVERSED_EVALS_MPTR = 0x5260; + uint256 internal constant SELECTOR_ACC_MPTR = 0x5f40; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x5f40; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0x63a0; + uint256 internal constant TRACE_U256_MPTR = 0x7940; // ---------------------------------------------------------------------- // Per-category bases for EIP-2537 padded G1 commitments. The proof @@ -170,13 +216,63 @@ contract Halo2Verifier { // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans // ---------------------------------------------------------------------- - uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x4840; - uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x4ac0; - uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x4b40; - uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x4cc0; - uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x4d40; - uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x4dc0; - uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x4dc0; + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x57c0; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x5a40; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x5ac0; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x5c40; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x5cc0; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x5d40; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x5d40; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 299628; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0xc8ebd03a9db607769309801d4400ca32e5c4da41621ca090e5ec163f5855d744; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. @@ -195,10 +291,19 @@ contract Halo2Verifier { /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + // Scratch is reused for every runtime-prerequisite probe. - let scratch := 0x80 + let scratch := 0x1000 // MCOPY must be available because the verifier uses it for // proof-time point/scratch staging. Execute the opcode here so a @@ -216,23 +321,144 @@ contract Halo2Verifier { // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. - let msm_scratch := 0x4fc0 + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0x5f40 for { let off := 0 } lt(off, 0x19a0) { off := add(off, 0x20) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x19a0, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x19a0, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -242,7 +468,7 @@ contract Halo2Verifier { // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } @@ -271,17 +497,33 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. @@ -298,7 +540,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } // Non-embedded renders pin the VK by address and codehash. The Yul @@ -306,24 +551,48 @@ contract Halo2Verifier { // INVALID-prefixed payload into VK_MPTR. address vk = AUTHORIZED_VK; assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== - // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of // a fixed post-VK address that can collide with live PCS scratch // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } - let p := 0x15e0 + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x2560 // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] mstore(add(p, 0x00), 0x20) // base len @@ -332,8 +601,8 @@ contract Halo2Verifier { mstore(add(p, 0x60), x) mstore(add(p, 0x80), sub(FR_MODULUS, 2)) mstore(add(p, 0xa0), FR_MODULUS) - if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -393,16 +662,16 @@ contract Halo2Verifier { let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -458,6 +727,13 @@ contract Halo2Verifier { // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -470,7 +746,7 @@ contract Halo2Verifier { mstore(add(single_scratch, 0x60), x) mstore(add(single_scratch, 0x80), sub(r, 2)) mstore(add(single_scratch, 0xa0), r) - ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) if ret { mstore(mptr_start, mload(single_scratch)) } leave @@ -478,16 +754,34 @@ contract Halo2Verifier { // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -502,8 +796,14 @@ contract Halo2Verifier { mstore(add(gp_mptr, 0x60), gp) mstore(add(gp_mptr, 0x80), sub(r, 2)) mstore(add(gp_mptr, 0xa0), r) - ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -528,22 +828,31 @@ contract Halo2Verifier { // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to // be a 4-step mstore chain for each G1 (~60 gas) and an // 8-iter mstore loop for each G2 (~240 gas). Net saving // here is ~500 gas per ec_pairing call. - let scratch := 0x0300 + let scratch := 0x1240 mcopy(scratch, lhs_mptr, 0x80) mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } @@ -574,7 +883,13 @@ contract Halo2Verifier { // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -742,6 +1057,14 @@ contract Halo2Verifier { // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -802,7 +1125,7 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -819,7 +1142,7 @@ contract Halo2Verifier { success := and(success, eq(mload(ACC_OFFSET_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 0)) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -843,7 +1166,7 @@ contract Halo2Verifier { ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } // =============================================================== @@ -913,7 +1236,7 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } // =============================================================== @@ -1068,7 +1391,7 @@ contract Halo2Verifier { // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, 0x20) @@ -1121,7 +1444,7 @@ contract Halo2Verifier { {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) proof_cptr := add(proof_cptr, 0x20) } @@ -1147,11 +1470,11 @@ contract Halo2Verifier { // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1170,8 +1493,10 @@ contract Halo2Verifier { // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, 0x03c0) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1181,11 +1506,11 @@ contract Halo2Verifier { } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1196,9 +1521,9 @@ contract Halo2Verifier { // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, 0x0100) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0100) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -1221,8 +1546,8 @@ contract Halo2Verifier { // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, 0x0100)) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0100)) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -1232,13 +1557,24 @@ contract Halo2Verifier { mstore(INSTANCE_EVAL_MPTR, instance_eval) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } - // Optional quotient helper functions. Each one is rendered only + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr // helpers and share the same FR_MODULUS as the surrounding - // numerator block. // =============================================================== + // numerator block. + // =============================================================== // Batched identity numerator / linearization target. // // This block does not evaluate the quotient polynomial h(x), and @@ -1334,15 +1670,15 @@ contract Halo2Verifier { // q_const_mptr points to Fr constants used by the VM. // q_program_mptr points to the bytecode stream. // Constants are stored as consecutive 32-byte Fr words. - let q_const_mptr := 0x1ac0 + let q_const_mptr := 0x2a40 // Program bytes are also stored in the VK payload, packed into // 32-byte words by PackedProgramCodec. - let q_program_mptr := 0x1ac0 + let q_program_mptr := 0x2a40 // Running Horner accumulator for fully evaluated identities. // After all identities, this is nu_y(x) for the `None` // identity group. // Initialize A = 0 before scanning the identity stream. - mstore(0x5020, 0) + mstore(0x5fa0, 0) // Simple selectors are grouped into separate linearization // buckets. They start at zero for every proof. // q_sel_zero_off walks selector bucket byte offsets. @@ -1357,12 +1693,19 @@ contract Halo2Verifier { { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0x5fe0, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, 15) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) // Store y^i at selector_power_mptr + 32*i. - mstore(add(0x5060, shl(5, q_y_power_i)), q_y_power) + mstore(add(0x5fe0, shl(5, q_y_power_i)), q_y_power) } } @@ -1371,102 +1714,102 @@ contract Halo2Verifier { // VM/native identities, so they occupy the same y-batch order. { let var0 := 0x1 - let f_3 := mload(0x4520) - let f_4 := mload(0x4420) - let a_0 := mload(0x4300) + let f_3 := mload(0x54a0) + let f_4 := mload(0x53a0) + let a_0 := mload(0x5280) let var1 := mulmod(f_4, a_0, r) let var2 := addmod(f_3, var1, r) - let f_5 := mload(0x4440) - let a_1 := mload(0x4320) + let f_5 := mload(0x53c0) + let a_1 := mload(0x52a0) let var3 := mulmod(f_5, a_1, r) let var4 := addmod(var2, var3, r) - let f_6 := mload(0x4460) - let a_2 := mload(0x4340) + let f_6 := mload(0x53e0) + let a_2 := mload(0x52c0) let var5 := mulmod(f_6, a_2, r) let var6 := addmod(var4, var5, r) - let f_7 := mload(0x4480) - let a_3 := mload(0x4360) + let f_7 := mload(0x5400) + let a_3 := mload(0x52e0) let var7 := mulmod(f_7, a_3, r) let var8 := addmod(var6, var7, r) - let f_8 := mload(0x44a0) - let a_4 := mload(0x4380) + let f_8 := mload(0x5420) + let a_4 := mload(0x5300) let var9 := mulmod(f_8, a_4, r) let var10 := addmod(var8, var9, r) - let f_0 := mload(0x44c0) - let a_0_next_1 := mload(0x43a0) + let f_0 := mload(0x5440) + let a_0_next_1 := mload(0x5320) let var11 := mulmod(f_0, a_0_next_1, r) let var12 := addmod(var10, var11, r) - let f_1 := mload(0x44e0) + let f_1 := mload(0x5460) let var13 := mulmod(f_1, a_0, r) let var14 := mulmod(var13, a_1, r) let var15 := addmod(var12, var14, r) - let f_2 := mload(0x4500) + let f_2 := mload(0x5480) let var16 := mulmod(f_2, a_0, r) let var17 := mulmod(var16, a_2, r) let var18 := addmod(var15, var17, r) let var19 := mulmod(var0, var18, r) - mstore(0x5240, var19) + mstore(0x61c0, var19) } - mstore(0x5020, mulmod(mload(0x5020), y, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x5240), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x61c0), r)) } { let var0 := 0x1 - let a_1 := mload(0x4320) - let a_2 := mload(0x4340) + let a_1 := mload(0x52a0) + let a_2 := mload(0x52c0) let var1 := addmod(a_1, a_2, r) - let a_3 := mload(0x4360) + let a_3 := mload(0x52e0) let var2 := addmod(0, sub(r, a_3), r) let var3 := addmod(var1, var2, r) - let a_4 := mload(0x4380) + let a_4 := mload(0x5300) let var4 := addmod(0, sub(r, a_4), r) let var5 := addmod(var3, var4, r) let var6 := mulmod(var0, var5, r) - mstore(0x5240, var6) + mstore(0x61c0, var6) } - mstore(0x5020, mulmod(mload(0x5020), y, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x5240), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x61c0), r)) } { let var0 := 0x1 - let a_0 := mload(0x4300) - let f_4 := mload(0x4420) + let a_0 := mload(0x5280) + let f_4 := mload(0x53a0) let var1 := addmod(a_0, f_4, r) - let a_0_next_1 := mload(0x43a0) + let a_0_next_1 := mload(0x5320) let var2 := addmod(0, sub(r, a_0_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x5240, var4) + mstore(0x61c0, var4) } - mstore(0x5020, mulmod(mload(0x5020), y, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x5240), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x61c0), r)) } { let var0 := 0x1 - let a_1 := mload(0x4320) - let f_5 := mload(0x4440) + let a_1 := mload(0x52a0) + let f_5 := mload(0x53c0) let var1 := addmod(a_1, f_5, r) - let a_1_next_1 := mload(0x43c0) + let a_1_next_1 := mload(0x5340) let var2 := addmod(0, sub(r, a_1_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x5240, var4) + mstore(0x61c0, var4) } - mstore(0x5020, mulmod(mload(0x5020), y, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5060, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x5240), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x5fe0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x61c0), r)) } // VM registers: @@ -1486,24 +1829,19 @@ contract Halo2Verifier { // q_end is an exclusive byte pointer for the VM loop. let q_end := add(q_program_mptr, 0x05) // q_sp starts at the first free stack word. - let q_sp := 0x5240 + let q_sp := 0x61c0 // q_top is meaningless until q_has_top is set. let q_top := 0 // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -1536,69 +1874,69 @@ contract Halo2Verifier { // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := 0x5240 + q_sp := 0x61c0 // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. { let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 - let q_perm_vals := 0x5240 - let q_perm_sigmas := 0x5340 - let q_perm_z_cur := 0x5440 - let q_perm_z_next := 0x54a0 - let q_perm_z_last := 0x5500 - let q_perm_delta_base_ptr := 0x5540 + let q_perm_vals := 0x61c0 + let q_perm_sigmas := 0x62c0 + let q_perm_z_cur := 0x63c0 + let q_perm_z_next := 0x6420 + let q_perm_z_last := 0x6480 + let q_perm_delta_base_ptr := 0x64c0 let q_perm_num_cols := 8 let q_perm_num_sets := 3 let q_perm_chunk_len := 3 let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 - mstore(add(q_perm_vals, 0x0), mload(0x4400)) + mstore(add(q_perm_vals, 0x0), mload(0x5380)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x4300, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x5280, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0xc0), mload(0x42e0)) + mstore(add(q_perm_vals, 0xc0), mload(0x5260)) mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) { for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 8) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off - mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x45c0, q_perm_sigma_load_src_off))) + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x5540, q_perm_sigma_load_src_off))) } } { for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 3) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) - mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x46c0, q_perm_z_cur_load_src_off))) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x5640, q_perm_z_cur_load_src_off))) } } { for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 3) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) - mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x46e0, q_perm_z_next_load_src_off))) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x5660, q_perm_z_next_load_src_off))) } } - mstore(add(q_perm_z_last, 0x0), mload(0x4700)) - mstore(add(q_perm_z_last, 0x20), mload(0x4760)) + mstore(add(q_perm_z_last, 0x0), mload(0x5680)) + mstore(add(q_perm_z_last, 0x20), mload(0x56e0)) let q_perm_eval := 0 q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_perm_eval, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_perm_eval, r)) let q_perm_zn := mload(add(q_perm_z_cur, 0x40)) q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_perm_eval, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_perm_eval, r)) for { let q_perm_i := 1 } lt(q_perm_i, 3) { q_perm_i := add(q_perm_i, 1) } { let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_perm_eval, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_perm_eval, r)) } mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) for { let q_perm_set := 0 } lt(q_perm_set, 3) { q_perm_set := add(q_perm_set, 1) } { @@ -1617,8 +1955,8 @@ contract Halo2Verifier { q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) } q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_perm_eval, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_perm_eval, r)) mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) } } @@ -1639,13 +1977,13 @@ contract Halo2Verifier { // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := 0x5240 + q_sp := 0x61c0 // Generated LogUp code follows the same y-batch order // as the Rust identity stream. { - let q_lookup_f := 0x5240 - let q_lookup_prefix := 0x52c0 - let q_lookup_suffix := 0x5340 + let q_lookup_f := 0x61c0 + let q_lookup_prefix := 0x6240 + let q_lookup_suffix := 0x62c0 let q_lookup_l0 := mload(L_0_MPTR) let q_lookup_llast := mload(L_LAST_MPTR) let q_lookup_lblind := mload(L_BLIND_MPTR) @@ -1655,17 +1993,17 @@ contract Halo2Verifier { let q_lookup_theta := mload(THETA_MPTR) { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x4800), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x5780), r) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_lookup_eval, r)) } { - let f_10 := mload(0x4540) + let f_10 := mload(0x54c0) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) let var1 := mulmod(var0, q_lookup_theta, r) for { let q_lookup_shared_i := 0 } lt(q_lookup_shared_i, 4) { q_lookup_shared_i := add(q_lookup_shared_i, 1) } { let q_lookup_shared_off := shl(5, q_lookup_shared_i) - let q_lookup_shared_tail := mload(add(0x4320, q_lookup_shared_off)) + let q_lookup_shared_tail := mload(add(0x52a0, q_lookup_shared_off)) let q_lookup_shared_compressed := addmod(var1, q_lookup_shared_tail, r) mstore(add(q_lookup_f, q_lookup_shared_off), addmod(q_lookup_shared_compressed, q_lookup_beta, r)) } @@ -1687,24 +2025,24 @@ contract Halo2Verifier { for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 4) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) } - let q_lookup_eval := addmod(mulmod(mload(0x47e0), q_lookup_product, r), sub(r, q_lookup_sum), r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x5760), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x47e0) - let f_16 := mload(0x45a0) - let f_11 := mload(0x4560) + let q_lookup_sum_h := mload(0x5760) + let f_16 := mload(0x5520) + let f_11 := mload(0x54e0) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) - let f_12 := mload(0x4580) + let f_12 := mload(0x5500) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) let q_lookup_s_sum_h := mulmod(f_16, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x4820), sub(r, addmod(mload(0x4800), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x57a0), sub(r, addmod(mload(0x5780), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x47c0), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x5740), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x5020, mulmod(mload(0x5020), y, r)) - mstore(0x5020, addmod(mload(0x5020), q_lookup_eval, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) + mstore(0x5fa0, addmod(mload(0x5fa0), q_lookup_eval, r)) } } } @@ -1725,42 +2063,48 @@ contract Halo2Verifier { // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := 0x5240 + q_sp := 0x61c0 // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx case 0 { { let var0 := 0x1 - let a_2 := mload(0x4340) - let f_6 := mload(0x4460) + let a_2 := mload(0x52c0) + let f_6 := mload(0x53e0) let var1 := addmod(a_2, f_6, r) - let a_2_next_1 := mload(0x43e0) + let a_2_next_1 := mload(0x5360) let var2 := addmod(0, sub(r, a_2_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x5240, var4) + mstore(0x61c0, var4) } - mstore(0x5020, mulmod(mload(0x5020), y, r)) + mstore(0x5fa0, mulmod(mload(0x5fa0), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x5060, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x5240), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x5fe0, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x61c0), r)) } } - default { revert(0, 0) } + default { q_program_fail() } } // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0x61c0)) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -1778,21 +2122,21 @@ contract Halo2Verifier { // selector commitment in the linearized MSM. { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5060, 0x01c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5fe0, 0x01c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5060, 0x01a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5fe0, 0x01a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5060, 0x0140)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x5fe0, 0x0140)), r)) } // Fully evaluated identities are the constant-polynomial side // of the linearization query. Rust subtracts that grouped // scalar into expected_eval, so Solidity stores -nu_y(x). - let linearization_expected_eval := addmod(0, sub(r, mload(0x5020)), r) + let linearization_expected_eval := addmod(0, sub(r, mload(0x5fa0)), r) mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } @@ -1891,39 +2235,39 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[0]: 28 commitment(s) (rolled, m>=4) + // q_eval_set[0]: 28 evaluation term(s), 27 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x5020, 0x4360) - mstore(0x5040, 0x4380) - mstore(0x5060, 0x42e0) - mstore(0x5080, 0x47c0) - mstore(0x50a0, 0x47e0) - mstore(0x50c0, 0x4400) - mstore(0x50e0, 0x4420) - mstore(0x5100, 0x4440) - mstore(0x5120, 0x4460) - mstore(0x5140, 0x4480) - mstore(0x5160, 0x44a0) - mstore(0x5180, 0x44c0) - mstore(0x51a0, 0x44e0) - mstore(0x51c0, 0x4500) - mstore(0x51e0, 0x4520) - mstore(0x5200, 0x4540) - mstore(0x5220, 0x4560) - mstore(0x5240, 0x4580) - mstore(0x5260, 0x45a0) - mstore(0x5280, 0x45c0) - mstore(0x52a0, 0x45e0) - mstore(0x52c0, 0x4600) - mstore(0x52e0, 0x4620) - mstore(0x5300, 0x4640) - mstore(0x5320, 0x4660) - mstore(0x5340, 0x4680) - mstore(0x5360, 0x46a0) - mstore(0x5380, QUOTIENT_EVAL_MPTR) - let q_eval_set_0 := mload(0x4360) + mstore(0x5fa0, 0x52e0) + mstore(0x5fc0, 0x5300) + mstore(0x5fe0, 0x5260) + mstore(0x6000, 0x5740) + mstore(0x6020, 0x5760) + mstore(0x6040, 0x5380) + mstore(0x6060, 0x53a0) + mstore(0x6080, 0x53c0) + mstore(0x60a0, 0x53e0) + mstore(0x60c0, 0x5400) + mstore(0x60e0, 0x5420) + mstore(0x6100, 0x5440) + mstore(0x6120, 0x5460) + mstore(0x6140, 0x5480) + mstore(0x6160, 0x54a0) + mstore(0x6180, 0x54c0) + mstore(0x61a0, 0x54e0) + mstore(0x61c0, 0x5500) + mstore(0x61e0, 0x5520) + mstore(0x6200, 0x5540) + mstore(0x6220, 0x5560) + mstore(0x6240, 0x5580) + mstore(0x6260, 0x55a0) + mstore(0x6280, 0x55c0) + mstore(0x62a0, 0x55e0) + mstore(0x62c0, 0x5600) + mstore(0x62e0, 0x5620) + mstore(0x6300, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x52e0) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x5020, 0x20) + let eval_p := add(0x5fa0, 0x20) for { let i := 1 } lt(i, 0x1c) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -1936,22 +2280,22 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[1]: 5 commitment(s) (rolled, m>=4) + // q_eval_set[1]: 5 evaluation term(s), 5 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x5020, 0x4300) - mstore(0x5040, 0x43a0) - mstore(0x5060, 0x4320) - mstore(0x5080, 0x43c0) - mstore(0x50a0, 0x4340) - mstore(0x50c0, 0x43e0) - mstore(0x50e0, 0x4780) - mstore(0x5100, 0x47a0) - mstore(0x5120, 0x4800) - mstore(0x5140, 0x4820) - let q_eval_set_0 := mload(0x4300) - let q_eval_set_1 := mload(0x43a0) + mstore(0x5fa0, 0x5280) + mstore(0x5fc0, 0x5320) + mstore(0x5fe0, 0x52a0) + mstore(0x6000, 0x5340) + mstore(0x6020, 0x52c0) + mstore(0x6040, 0x5360) + mstore(0x6060, 0x5700) + mstore(0x6080, 0x5720) + mstore(0x60a0, 0x5780) + mstore(0x60c0, 0x57a0) + let q_eval_set_0 := mload(0x5280) + let q_eval_set_1 := mload(0x5320) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x5020, 0x40) + let eval_p := add(0x5fa0, 0x40) for { let i := 1 } lt(i, 0x5) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -1966,13 +2310,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[2]: 2 commitment(s) - let q_eval_set_0 := mload(0x46c0) - let q_eval_set_1 := mload(0x46e0) - let q_eval_set_2 := mload(0x4700) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x4720), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x4740), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x4760), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + // q_eval_set[2]: 2 evaluation term(s), 2 commitment term(s) + let q_eval_set_0 := mload(0x5640) + let q_eval_set_1 := mload(0x5660) + let q_eval_set_2 := mload(0x5680) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x56a0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x56c0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x56e0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x60), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x80), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0xa0), q_eval_set_2) @@ -2092,95 +2436,96 @@ contract Halo2Verifier { v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x20)), x4_pow_1, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_3, r), r) - mcopy(0x5020, 0x49c0, 0x80) - mstore(0x50a0, 1) - mcopy(0x50c0, 0x4a40, 0x80) - mstore(0x5140, mload(add(X1_POWERS_MPTR, 0x20))) - mcopy(0x5160, 0x4ac0, 0x80) - mstore(0x51e0, mload(add(X1_POWERS_MPTR, 0x60))) - mcopy(0x5200, 0x4cc0, 0x80) - mstore(0x5280, mload(add(X1_POWERS_MPTR, 0x80))) - mcopy(0x52a0, 0x1f60, 0x80) - mstore(0x5320, mload(add(X1_POWERS_MPTR, 0xa0))) - mcopy(0x5340, 0x1ce0, 0x80) - mstore(0x53c0, mload(add(X1_POWERS_MPTR, 0xc0))) - mcopy(0x53e0, 0x1d60, 0x80) - mstore(0x5460, mload(add(X1_POWERS_MPTR, 0xe0))) - mcopy(0x5480, 0x1de0, 0x80) - mstore(0x5500, mload(add(X1_POWERS_MPTR, 0x100))) - mcopy(0x5520, 0x1e60, 0x80) - mstore(0x55a0, mload(add(X1_POWERS_MPTR, 0x120))) - mcopy(0x55c0, 0x1ee0, 0x80) - mstore(0x5640, mload(add(X1_POWERS_MPTR, 0x140))) - mcopy(0x5660, 0x1ae0, 0x80) - mstore(0x56e0, mload(add(X1_POWERS_MPTR, 0x160))) - mcopy(0x5700, 0x1b60, 0x80) - mstore(0x5780, mload(add(X1_POWERS_MPTR, 0x180))) - mcopy(0x57a0, 0x1be0, 0x80) - mstore(0x5820, mload(add(X1_POWERS_MPTR, 0x1a0))) - mcopy(0x5840, 0x1c60, 0x80) - mstore(0x58c0, mload(add(X1_POWERS_MPTR, 0x1c0))) - mcopy(0x58e0, 0x1fe0, 0x80) - mstore(0x5960, mload(add(X1_POWERS_MPTR, 0x1e0))) - mcopy(0x5980, 0x2060, 0x80) - mstore(0x5a00, mload(add(X1_POWERS_MPTR, 0x200))) - mcopy(0x5a20, 0x20e0, 0x80) - mstore(0x5aa0, mload(add(X1_POWERS_MPTR, 0x220))) - mcopy(0x5ac0, 0x22e0, 0x80) - mstore(0x5b40, mload(add(X1_POWERS_MPTR, 0x240))) - mcopy(0x5b60, 0x2360, 0x80) - mstore(0x5be0, mload(add(X1_POWERS_MPTR, 0x260))) - mcopy(0x5c00, 0x23e0, 0x80) - mstore(0x5c80, mload(add(X1_POWERS_MPTR, 0x280))) - mcopy(0x5ca0, 0x2460, 0x80) - mstore(0x5d20, mload(add(X1_POWERS_MPTR, 0x2a0))) - mcopy(0x5d40, 0x24e0, 0x80) - mstore(0x5dc0, mload(add(X1_POWERS_MPTR, 0x2c0))) - mcopy(0x5de0, 0x2560, 0x80) - mstore(0x5e60, mload(add(X1_POWERS_MPTR, 0x2e0))) - mcopy(0x5e80, 0x25e0, 0x80) - mstore(0x5f00, mload(add(X1_POWERS_MPTR, 0x300))) - mcopy(0x5f20, 0x2660, 0x80) - mstore(0x5fa0, mload(add(X1_POWERS_MPTR, 0x320))) - mcopy(0x5fc0, 0x26e0, 0x80) - mstore(0x6040, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0x5fa0, 0x5940, 0x80) + mstore(0x6020, 1) + mcopy(0x6040, 0x59c0, 0x80) + mstore(0x60c0, mload(add(X1_POWERS_MPTR, 0x20))) + mcopy(0x60e0, 0x5a40, 0x80) + mstore(0x6160, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0x6180, 0x5c40, 0x80) + mstore(0x6200, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0x6220, 0x2ee0, 0x80) + mstore(0x62a0, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0x62c0, 0x2c60, 0x80) + mstore(0x6340, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0x6360, 0x2ce0, 0x80) + mstore(0x63e0, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0x6400, 0x2d60, 0x80) + mstore(0x6480, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0x64a0, 0x2de0, 0x80) + mstore(0x6520, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0x6540, 0x2e60, 0x80) + mstore(0x65c0, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0x65e0, 0x2a60, 0x80) + mstore(0x6660, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0x6680, 0x2ae0, 0x80) + mstore(0x6700, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0x6720, 0x2b60, 0x80) + mstore(0x67a0, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0x67c0, 0x2be0, 0x80) + mstore(0x6840, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0x6860, 0x2f60, 0x80) + mstore(0x68e0, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0x6900, 0x2fe0, 0x80) + mstore(0x6980, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0x69a0, 0x3060, 0x80) + mstore(0x6a20, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0x6a40, 0x3260, 0x80) + mstore(0x6ac0, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0x6ae0, 0x32e0, 0x80) + mstore(0x6b60, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0x6b80, 0x3360, 0x80) + mstore(0x6c00, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0x6c20, 0x33e0, 0x80) + mstore(0x6ca0, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0x6cc0, 0x3460, 0x80) + mstore(0x6d40, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0x6d60, 0x34e0, 0x80) + mstore(0x6de0, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0x6e00, 0x3560, 0x80) + mstore(0x6e80, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0x6ea0, 0x35e0, 0x80) + mstore(0x6f20, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0x6f40, 0x3660, 0x80) + mstore(0x6fc0, mload(add(X1_POWERS_MPTR, 0x340))) let lin_query_scalar_26 := mload(add(X1_POWERS_MPTR, 0x360)) let lin_cur_scalar_26 := mulmod(lin_query_scalar_26, lin_one_minus_x_n, r) - mcopy(0x6060, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) - mstore(0x60e0, lin_cur_scalar_26) + mcopy(0x6fe0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0x7060, lin_cur_scalar_26) lin_cur_scalar_26 := mulmod(lin_cur_scalar_26, lin_x_split, r) - mcopy(0x6100, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) - mstore(0x6180, lin_cur_scalar_26) + mcopy(0x7080, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0x7100, lin_cur_scalar_26) lin_cur_scalar_26 := mulmod(lin_cur_scalar_26, lin_x_split, r) - mcopy(0x61a0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) - mstore(0x6220, lin_cur_scalar_26) + mcopy(0x7120, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0x71a0, lin_cur_scalar_26) lin_cur_scalar_26 := mulmod(lin_cur_scalar_26, lin_x_split, r) - mcopy(0x6240, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) - mstore(0x62c0, lin_cur_scalar_26) - mcopy(0x62e0, 0x2160, 0x80) - mstore(0x6360, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) - mcopy(0x6380, 0x21e0, 0x80) - mstore(0x6400, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) - mcopy(0x6420, 0x2260, 0x80) - mstore(0x64a0, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) - mcopy(0x64c0, 0x4840, 0x80) - mstore(0x6540, x4_pow_1) - mcopy(0x6560, 0x48c0, 0x80) - mstore(0x65e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) - mcopy(0x6600, 0x4940, 0x80) - mstore(0x6680, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) - mcopy(0x66a0, 0x4c40, 0x80) - mstore(0x6720, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_1, r)) - mcopy(0x6740, 0x4d40, 0x80) - mstore(0x67c0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_1, r)) - mcopy(0x67e0, 0x4b40, 0x80) - mstore(0x6860, x4_pow_2) - mcopy(0x6880, 0x4bc0, 0x80) - mstore(0x6900, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) - mcopy(0x6920, F_COM_MPTR, 0x80) - mstore(0x69a0, x4_pow_3) + mcopy(0x71c0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0x7240, lin_cur_scalar_26) + mcopy(0x7260, 0x30e0, 0x80) + mstore(0x72e0, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0x7300, 0x3160, 0x80) + mstore(0x7380, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0x73a0, 0x31e0, 0x80) + mstore(0x7420, mulmod(lin_query_scalar_26, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0x7440, 0x57c0, 0x80) + mstore(0x74c0, x4_pow_1) + mcopy(0x74e0, 0x5840, 0x80) + mstore(0x7560, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0x7580, 0x58c0, 0x80) + mstore(0x7600, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0x7620, 0x5bc0, 0x80) + mstore(0x76a0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_1, r)) + mcopy(0x76c0, 0x5cc0, 0x80) + mstore(0x7740, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_1, r)) + mcopy(0x7760, 0x5ac0, 0x80) + mstore(0x77e0, x4_pow_2) + mcopy(0x7800, 0x5b40, 0x80) + mstore(0x7880, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0x78a0, F_COM_MPTR, 0x80) + mstore(0x7920, x4_pow_3) if success { - success := staticcall(gas(), 0x0c, 0x5020, 0x19a0, FINAL_COM_MPTR, 0x80) + // exact EIP-2537 G1MSM cost for 41 pair(s) + success := staticcall(299628, 0x0c, 0x5fa0, 0x19a0, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) @@ -2192,28 +2537,28 @@ contract Halo2Verifier { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) - mcopy(0x80, G1_BASE_MPTR, 0x80) - mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, FINAL_COM_MPTR, 0x80) + mcopy(0x1080, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, PI_MPTR, 0x80) - mstore(0x180, mload(X3_MPTR)) + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) if success { - success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) } } @@ -2244,13 +2589,19 @@ contract Halo2Verifier { // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) } diff --git a/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2VerifyingKey.sol b/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2VerifyingKey.sol index cd266f402..056663339 100644 --- a/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/target/rsa-signature-fixture-dump/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. diff --git a/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2Verifier.sol b/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2Verifier.sol index 2e9d4419e..b418c826b 100644 --- a/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2Verifier.sol +++ b/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,34 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice Verifying-key contract address authorized for this verifier. /// @dev The runtime length and codehash are pinned by generated constants and checked at construction time. @@ -43,7 +82,7 @@ contract Halo2Verifier { // EXPECTED_VK_PAYLOAD_LENGTH. uint256 internal constant EXPECTED_VK_PAYLOAD_LENGTH = 8032; uint256 internal constant EXPECTED_VK_LENGTH = 8033; - uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x6200af6d8b2f00ac28582e3c159c74e3e43daf38a03210efc438aa723d3e98bc; + uint256 internal constant EXPECTED_VK_CODEHASH_WORD = 0x677b7b26592c167d44fd428a1d743abb1eb2fd8040804bd032440791caeb7471; bytes32 internal constant EXPECTED_VK_CODEHASH = bytes32(EXPECTED_VK_CODEHASH_WORD); // Solidity ABI calldata cursors. The generated verifier accepts exactly @@ -55,8 +94,8 @@ contract Halo2Verifier { uint256 internal constant INSTANCE_CPTR = 0x1504; // First general-purpose memory words reserved by the generated verifier. // RETURN_MPTR is a single word set to 1 on success. - uint256 internal constant TRANSCRIPT_MPTR = 0x80; - uint256 internal constant RETURN_MPTR = 0x80; + uint256 internal constant TRANSCRIPT_MPTR = 0x1000; + uint256 internal constant RETURN_MPTR = 0x1000; // ---------------------------------------------------------------------- // Verifying-key memory map. The VK header lives at VK_MPTR, followed @@ -64,84 +103,87 @@ contract Halo2Verifier { // runtime comes the challenge slots (challenge_mptr..) and the // per-stage scratch (theta_mptr..). // ---------------------------------------------------------------------- - uint256 internal constant VK_MPTR = 0x1d20; - uint256 internal constant VK_DIGEST_MPTR = 0x1d20; - uint256 internal constant NUM_INSTANCES_MPTR = 0x1d40; - uint256 internal constant K_MPTR = 0x1d60; - uint256 internal constant N_INV_MPTR = 0x1d80; - uint256 internal constant OMEGA_MPTR = 0x1da0; - uint256 internal constant OMEGA_INV_MPTR = 0x1dc0; - uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x1de0; - uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x1e00; - uint256 internal constant ACC_OFFSET_MPTR = 0x1e20; - uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x1e40; - uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x1e60; - uint256 internal constant G1_BASE_MPTR = 0x1e80; - uint256 internal constant G2_BASE_MPTR = 0x1f00; - uint256 internal constant NEG_S_G2_BASE_MPTR = 0x2000; - - uint256 internal constant CHALLENGE_MPTR = 0x3c80; + uint256 internal constant VK_MPTR = 0x2ca0; + uint256 internal constant VK_DIGEST_MPTR = 0x2ca0; + uint256 internal constant NUM_INSTANCES_MPTR = 0x2cc0; + uint256 internal constant K_MPTR = 0x2ce0; + uint256 internal constant N_INV_MPTR = 0x2d00; + uint256 internal constant OMEGA_MPTR = 0x2d20; + uint256 internal constant OMEGA_INV_MPTR = 0x2d40; + uint256 internal constant OMEGA_INV_TO_L_MPTR = 0x2d60; + uint256 internal constant HAS_ACCUMULATOR_MPTR = 0x2d80; + uint256 internal constant ACC_OFFSET_MPTR = 0x2da0; + uint256 internal constant NUM_ACC_LIMBS_MPTR = 0x2dc0; + uint256 internal constant NUM_ACC_LIMB_BITS_MPTR = 0x2de0; + uint256 internal constant G1_BASE_MPTR = 0x2e00; + uint256 internal constant G2_BASE_MPTR = 0x2e80; + uint256 internal constant NEG_S_G2_BASE_MPTR = 0x2f80; + + uint256 internal constant CHALLENGE_MPTR = 0x4c00; // Challenge layout. Squeeze order in midnight-proofs: // user_phase challenges (variable count) // theta -> beta, gamma -> trash_challenge -> y -> x -> // x1, x2 -> x3 -> x4 - uint256 internal constant THETA_MPTR = 0x3c80; - uint256 internal constant BETA_MPTR = 0x3ca0; - uint256 internal constant GAMMA_MPTR = 0x3cc0; - uint256 internal constant TRASH_CHALLENGE_MPTR = 0x3ce0; - uint256 internal constant Y_MPTR = 0x3d00; - uint256 internal constant X_MPTR = 0x3d20; - uint256 internal constant X1_MPTR = 0x3d40; - uint256 internal constant X2_MPTR = 0x3d60; - uint256 internal constant X3_MPTR = 0x3d80; - uint256 internal constant X4_MPTR = 0x3da0; + uint256 internal constant THETA_MPTR = 0x4c00; + uint256 internal constant BETA_MPTR = 0x4c20; + uint256 internal constant GAMMA_MPTR = 0x4c40; + uint256 internal constant TRASH_CHALLENGE_MPTR = 0x4c60; + uint256 internal constant Y_MPTR = 0x4c80; + uint256 internal constant X_MPTR = 0x4ca0; + uint256 internal constant X1_MPTR = 0x4cc0; + uint256 internal constant X2_MPTR = 0x4ce0; + uint256 internal constant X3_MPTR = 0x4d00; + uint256 internal constant X4_MPTR = 0x4d20; // Batch-open commitments live in 4-word EIP-2537 padded slots. - uint256 internal constant F_COM_MPTR = 0x3dc0; - uint256 internal constant PI_MPTR = 0x3e40; + uint256 internal constant F_COM_MPTR = 0x4d40; + uint256 internal constant PI_MPTR = 0x4dc0; // Accumulator (KZG IVC). - uint256 internal constant ACC_LHS_MPTR = 0x3ec0; - uint256 internal constant ACC_RHS_MPTR = 0x3f40; + uint256 internal constant ACC_LHS_MPTR = 0x4e40; + uint256 internal constant ACC_RHS_MPTR = 0x4ec0; // Lagrange / linearization scratch. - uint256 internal constant X_N_MPTR = 0x3fc0; - uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x3fe0; - uint256 internal constant L_LAST_MPTR = 0x4000; - uint256 internal constant L_BLIND_MPTR = 0x4020; - uint256 internal constant L_0_MPTR = 0x4040; - uint256 internal constant INSTANCE_EVAL_MPTR = 0x4060; + uint256 internal constant X_N_MPTR = 0x4f40; + uint256 internal constant X_N_MINUS_1_INV_MPTR = 0x4f60; + uint256 internal constant L_LAST_MPTR = 0x4f80; + uint256 internal constant L_BLIND_MPTR = 0x4fa0; + uint256 internal constant L_0_MPTR = 0x4fc0; + uint256 internal constant INSTANCE_EVAL_MPTR = 0x4fe0; // Legacy name: this is not h(x). It stores the expected opening // scalar for the linearized commitment, i.e. the negated y-batched // identity numerator reconstructed from the alleged evals at x. - uint256 internal constant QUOTIENT_EVAL_MPTR = 0x4080; - uint256 internal constant QUOTIENT_MPTR = 0x40a0; // 4 words - uint256 internal constant F_EVAL_MPTR = 0x4140; - uint256 internal constant V_MPTR = 0x4160; - uint256 internal constant FINAL_COM_MPTR = 0x4180; // 4 words - uint256 internal constant PAIRING_LHS_MPTR = 0x4200; // 4 words - uint256 internal constant PAIRING_RHS_MPTR = 0x4280; // 4 words + uint256 internal constant QUOTIENT_EVAL_MPTR = 0x5000; + uint256 internal constant QUOTIENT_MPTR = 0x5020; // 4 words + uint256 internal constant F_EVAL_MPTR = 0x50c0; + uint256 internal constant V_MPTR = 0x50e0; + uint256 internal constant FINAL_COM_MPTR = 0x5100; // 4 words + uint256 internal constant PAIRING_LHS_MPTR = 0x5180; // 4 words + uint256 internal constant PAIRING_RHS_MPTR = 0x5200; // 4 words // Multi-prepare scratch (sized at codegen time). - uint256 internal constant ROT_POINTS_MPTR = 0x4300; - uint256 internal constant X1_POWERS_MPTR = 0x4680; + uint256 internal constant ROT_POINTS_MPTR = 0x5280; + uint256 internal constant X1_POWERS_MPTR = 0x5600; // Q_COM materialization is currently fused into the final MSM scratch, // so this marker intentionally aliases Q_EVAL_SET_MPTR and has zero // reserved capacity until a future emitter starts writing Q_COM_MPTR. - uint256 internal constant Q_COM_MPTR = 0x4ea0; - uint256 internal constant Q_EVAL_SET_MPTR = 0x4ea0; + uint256 internal constant Q_COM_MPTR = 0x5e20; + uint256 internal constant Q_EVAL_SET_MPTR = 0x5e20; // Q_EVAL_CPTR is set at runtime once the verifier reaches the q_evals // block of the proof; we keep it as a memory slot for symmetry. - uint256 internal constant Q_EVAL_CPTR_MPTR = 0x55a0; + uint256 internal constant Q_EVAL_CPTR_MPTR = 0x6520; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. - uint256 internal constant G1_IDENTITY_MPTR = 0x56a0; + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. + uint256 internal constant G1_IDENTITY_MPTR = 0x6620; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain // Solidity proof shim rewrites proof scalars into canonical BE words, @@ -149,11 +191,15 @@ contract Halo2Verifier { // side `evaluations` loop range-checks and spills that value here so // downstream eval references (gate evaluator + PCS q_eval Horner) // become 3-gas `mload(...)` instead of calldata reads. - uint256 internal constant REVERSED_EVALS_MPTR = 0x5800; - uint256 internal constant SELECTOR_ACC_MPTR = 0x6b00; - uint256 internal constant QUOTIENT_RETURN_MPTR = 0x80; - uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x6b00; - uint256 internal constant TRACE_U256_MPTR = 0x9420; + uint256 internal constant REVERSED_EVALS_MPTR = 0x6780; + uint256 internal constant SELECTOR_ACC_MPTR = 0x7a80; + uint256 internal constant QUOTIENT_RETURN_MPTR = 0x1000; + uint256 internal constant BATCH_INV_SCRATCH_MPTR = 0x7a80; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = 0x8040; + uint256 internal constant TRACE_U256_MPTR = 0xa3a0; // ---------------------------------------------------------------------- // Per-category bases for EIP-2537 padded G1 commitments. The proof @@ -170,13 +216,63 @@ contract Halo2Verifier { // TRASHCAN_COMMS_MPTR_BASE + ... + 4*num_lookups // QUOTIENT_LIMB_COMMS_MPTR_BASE + ... + 4*num_trashcans // ---------------------------------------------------------------------- - uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x6080; - uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x6480; - uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x6580; - uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x6700; - uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x6800; - uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x6900; - uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x6900; + uint256 internal constant ADVICE_COMMS_MPTR_BASE = 0x7000; + uint256 internal constant LOOKUP_M_COMMS_MPTR_BASE = 0x7400; + uint256 internal constant PERM_Z_COMMS_MPTR_BASE = 0x7500; + uint256 internal constant LOOKUP_HELPER_COMMS_MPTR_BASE = 0x7680; + uint256 internal constant LOOKUP_Z_COMMS_MPTR_BASE = 0x7780; + uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = 0x7880; + uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = 0x7880; + + // ---------------------------------------------------------------------- + // Precompile gas bounds: the exact EIP-2537 / EIP-2565 scheduled costs. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // Liveness caveat: if a future fork reprices these precompiles UPWARD, + // this verifier must be regenerated and redeployed. The constructor + // smoke probes forward the same bounds, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = 375; + uint256 internal constant G1MSM_GAS_1PAIR = 12000; + uint256 internal constant PAIRING_GAS_2PAIR = 102900; + uint256 internal constant MODEXP_GAS = 1360; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = 436212; + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = 0x1c7b7ee0fa4c51d53a25031f26abb34b3e31f13dc83e83927465e52cb369a210; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. @@ -195,10 +291,19 @@ contract Halo2Verifier { /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact EIP-2537 gas bounds the runtime + /// uses (see the gas-bound constants block), so a chain whose + /// precompile schedule was repriced upward fails here, at deployment, + /// instead of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + if gt(mload(0x40), 0x1000) { revert(0, 0) } + // Scratch is reused for every runtime-prerequisite probe. - let scratch := 0x80 + let scratch := 0x1000 // MCOPY must be available because the verifier uses it for // proof-time point/scratch staging. Execute the opcode here so a @@ -216,23 +321,144 @@ contract Halo2Verifier { // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mcopy(add(scratch, 0x80), scratch, 0x80) + if iszero(staticcall(G1ADD_GAS, 0x0b, scratch, 0x0100, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, 0x0c, scratch, 0xa0, scratch, 0x80)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, 0x0c, scratch, 0xa0, scratch, 0x80) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, add(scratch, 0x300), 0x20)) { revert(0, 0) } + if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, 0x0300) { off := add(off, 0x20) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. - let msm_scratch := 0x6b00 + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. + let msm_scratch := 0x7a80 for { let off := 0 } lt(off, 0x2760) { off := add(off, 0x20) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), 0x0c, msm_scratch, 0x2760, scratch, 0x80)) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, 0x0c, msm_scratch, 0x2760, scratch, 0x80)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x80)) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -242,7 +468,7 @@ contract Halo2Verifier { // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20)) { revert(0, 0) } if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } @@ -271,17 +497,33 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. @@ -298,7 +540,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload(0x04), 0x40), eq(calldataload(0x24), sub(NUM_INSTANCE_CPTR, 0x04)))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } // Non-embedded renders pin the VK by address and codehash. The Yul @@ -306,24 +551,48 @@ contract Halo2Verifier { // INVALID-prefixed payload into VK_MPTR. address vk = AUTHORIZED_VK; assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + if gt(mload(0x40), TRANSCRIPT_MPTR) { revert(0, 0) } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== - // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of // a fixed post-VK address that can collide with live PCS scratch // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } - let p := 0x1c20 + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } + let p := 0x2ba0 // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] mstore(add(p, 0x00), 0x20) // base len @@ -332,8 +601,8 @@ contract Halo2Verifier { mstore(add(p, 0x60), x) mstore(add(p, 0x80), sub(FR_MODULUS, 2)) mstore(add(p, 0xa0), FR_MODULUS) - if iszero(staticcall(gas(), 0x05, p, 0xc0, p, 0x20)) { revert(0, 0) } - if iszero(eq(returndatasize(), 0x20)) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, 0x05, p, 0xc0, p, 0x20)) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), 0x20)) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -393,16 +662,16 @@ contract Halo2Verifier { let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -458,6 +727,13 @@ contract Halo2Verifier { // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -470,7 +746,7 @@ contract Halo2Verifier { mstore(add(single_scratch, 0x60), x) mstore(add(single_scratch, 0x80), sub(r, 2)) mstore(add(single_scratch, 0xa0), r) - ret := staticcall(gas(), 0x05, single_scratch, 0xc0, single_scratch, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, single_scratch, 0xc0, single_scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) if ret { mstore(mptr_start, mload(single_scratch)) } leave @@ -478,16 +754,34 @@ contract Halo2Verifier { // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -502,8 +796,14 @@ contract Halo2Verifier { mstore(add(gp_mptr, 0x60), gp) mstore(add(gp_mptr, 0x80), sub(r, 2)) mstore(add(gp_mptr, 0xa0), r) - ret := staticcall(gas(), 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) + ret := staticcall(MODEXP_GAS, 0x05, gp_mptr, 0xc0, gp_mptr, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -528,22 +828,31 @@ contract Halo2Verifier { // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to // be a 4-step mstore chain for each G1 (~60 gas) and an // 8-iter mstore loop for each G2 (~240 gas). Net saving // here is ~500 gas per ec_pairing call. - let scratch := 0x0300 + let scratch := 0x1240 mcopy(scratch, lhs_mptr, 0x80) mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), 0x0f, scratch, 0x0300, scratch, 0x20) + ret := staticcall(PAIRING_GAS_2PAIR, 0x0f, scratch, 0x0300, scratch, 0x20) ret := and(ret, eq(returndatasize(), 0x20)) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := and(ret, eq(mload(scratch), 1)) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } @@ -574,7 +883,13 @@ contract Halo2Verifier { // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -742,6 +1057,14 @@ contract Halo2Verifier { // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -802,7 +1125,7 @@ contract Halo2Verifier { if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -819,7 +1142,7 @@ contract Halo2Verifier { success := and(success, eq(mload(ACC_OFFSET_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), 0)) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), 0)) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -843,7 +1166,7 @@ contract Halo2Verifier { ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } // =============================================================== @@ -913,7 +1236,7 @@ contract Halo2Verifier { // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } // =============================================================== @@ -1084,7 +1407,7 @@ contract Halo2Verifier { // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, 0x20) @@ -1137,7 +1460,7 @@ contract Halo2Verifier { {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) proof_cptr := add(proof_cptr, 0x20) } @@ -1163,11 +1486,11 @@ contract Halo2Verifier { // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). @@ -1186,8 +1509,10 @@ contract Halo2Verifier { // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, 0x0520) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1197,11 +1522,11 @@ contract Halo2Verifier { } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -1212,9 +1537,9 @@ contract Halo2Verifier { // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, 0x0120) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, 0x0120) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -1237,8 +1562,8 @@ contract Halo2Verifier { // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, 0x0120)) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, 0x0120)) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -1248,13 +1573,24 @@ contract Halo2Verifier { mstore(INSTANCE_EVAL_MPTR, instance_eval) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } + + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } - // Optional quotient helper functions. Each one is rendered only + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr // helpers and share the same FR_MODULUS as the surrounding - // numerator block. // =============================================================== + // numerator block. + // =============================================================== // Batched identity numerator / linearization target. // // This block does not evaluate the quotient polynomial h(x), and @@ -1350,15 +1686,15 @@ contract Halo2Verifier { // q_const_mptr points to Fr constants used by the VM. // q_program_mptr points to the bytecode stream. // Constants are stored as consecutive 32-byte Fr words. - let q_const_mptr := 0x2100 + let q_const_mptr := 0x3080 // Program bytes are also stored in the VK payload, packed into // 32-byte words by PackedProgramCodec. - let q_program_mptr := 0x2660 + let q_program_mptr := 0x35e0 // Running Horner accumulator for fully evaluated identities. // After all identities, this is nu_y(x) for the `None` // identity group. // Initialize A = 0 before scanning the identity stream. - mstore(0x6cc0, 0) + mstore(0x7c40, 0) // Simple selectors are grouped into separate linearization // buckets. They start at zero for every proof. // q_sel_zero_off walks selector bucket byte offsets. @@ -1373,12 +1709,19 @@ contract Halo2Verifier { { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore(0x7c80, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, 35) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) // Store y^i at selector_power_mptr + 32*i. - mstore(add(0x6d00, shl(5, q_y_power_i)), q_y_power) + mstore(add(0x7c80, shl(5, q_y_power_i)), q_y_power) } } @@ -1387,102 +1730,102 @@ contract Halo2Verifier { // VM/native identities, so they occupy the same y-batch order. { let var0 := 0x1 - let f_3 := mload(0x5c40) - let f_4 := mload(0x5b40) - let a_0 := mload(0x5820) + let f_3 := mload(0x6bc0) + let f_4 := mload(0x6ac0) + let a_0 := mload(0x67a0) let var1 := mulmod(f_4, a_0, r) let var2 := addmod(f_3, var1, r) - let f_5 := mload(0x5b60) - let a_1 := mload(0x5840) + let f_5 := mload(0x6ae0) + let a_1 := mload(0x67c0) let var3 := mulmod(f_5, a_1, r) let var4 := addmod(var2, var3, r) - let f_6 := mload(0x5b80) - let a_2 := mload(0x5860) + let f_6 := mload(0x6b00) + let a_2 := mload(0x67e0) let var5 := mulmod(f_6, a_2, r) let var6 := addmod(var4, var5, r) - let f_7 := mload(0x5ba0) - let a_3 := mload(0x5880) + let f_7 := mload(0x6b20) + let a_3 := mload(0x6800) let var7 := mulmod(f_7, a_3, r) let var8 := addmod(var6, var7, r) - let f_8 := mload(0x5bc0) - let a_4 := mload(0x58a0) + let f_8 := mload(0x6b40) + let a_4 := mload(0x6820) let var9 := mulmod(f_8, a_4, r) let var10 := addmod(var8, var9, r) - let f_0 := mload(0x5be0) - let a_0_next_1 := mload(0x58c0) + let f_0 := mload(0x6b60) + let a_0_next_1 := mload(0x6840) let var11 := mulmod(f_0, a_0_next_1, r) let var12 := addmod(var10, var11, r) - let f_1 := mload(0x5c00) + let f_1 := mload(0x6b80) let var13 := mulmod(f_1, a_0, r) let var14 := mulmod(var13, a_1, r) let var15 := addmod(var12, var14, r) - let f_2 := mload(0x5c20) + let f_2 := mload(0x6ba0) let var16 := mulmod(f_2, a_0, r) let var17 := mulmod(var16, a_2, r) let var18 := addmod(var15, var17, r) let var19 := mulmod(var0, var18, r) - mstore(0x7160, var19) + mstore(0x80e0, var19) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } { let var0 := 0x1 - let a_1 := mload(0x5840) - let a_2 := mload(0x5860) + let a_1 := mload(0x67c0) + let a_2 := mload(0x67e0) let var1 := addmod(a_1, a_2, r) - let a_3 := mload(0x5880) + let a_3 := mload(0x6800) let var2 := addmod(0, sub(r, a_3), r) let var3 := addmod(var1, var2, r) - let a_4 := mload(0x58a0) + let a_4 := mload(0x6820) let var4 := addmod(0, sub(r, a_4), r) let var5 := addmod(var3, var4, r) let var6 := mulmod(var0, var5, r) - mstore(0x7160, var6) + mstore(0x80e0, var6) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x20) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } { let var0 := 0x1 - let a_0 := mload(0x5820) - let f_4 := mload(0x5b40) + let a_0 := mload(0x67a0) + let f_4 := mload(0x6ac0) let var1 := addmod(a_0, f_4, r) - let a_0_next_1 := mload(0x58c0) + let a_0_next_1 := mload(0x6840) let var2 := addmod(0, sub(r, a_0_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x7160, var4) + mstore(0x80e0, var4) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } { let var0 := 0x1 - let a_1 := mload(0x5840) - let f_5 := mload(0x5b60) + let a_1 := mload(0x67c0) + let f_5 := mload(0x6ae0) let var1 := addmod(a_1, f_5, r) - let a_1_next_1 := mload(0x58e0) + let a_1_next_1 := mload(0x6860) let var2 := addmod(0, sub(r, a_1_next_1), r) let var3 := addmod(var1, var2, r) let var4 := mulmod(var0, var3, r) - mstore(0x7160, var4) + mstore(0x80e0, var4) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x40) let q_selector_acc := mload(q_selector_ptr) - q_selector_acc := mulmod(q_selector_acc, mload(add(0x6d00, 0x20)), r) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + q_selector_acc := mulmod(q_selector_acc, mload(add(0x7c80, 0x20)), r) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } // VM registers: @@ -1502,24 +1845,30 @@ contract Halo2Verifier { // q_end is an exclusive byte pointer for the VM loop. let q_end := add(q_program_mptr, 0x019b) // q_sp starts at the first free stack word. - let q_sp := 0x7160 + let q_sp := 0x80e0 // q_top is meaningless until q_has_top is set. let q_top := 0 // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + // 0x05 push_mem_u16 + // 0x06 add + // 0x08 neg + // 0x09 push_const_u8 + // 0x0b fold_selector + // 0x0d mul_const_u8 + // 0x10 add_mem_u16 + // 0x11 mul_mem_u16 + // 0x13 add_mul_const_u8_mem_u16 + // 0x19 native_permutation + // 0x1f native_lookup + // 0x1b native_identity + // 0x1c lin7 + // 0x21 modarith7 // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -1543,6 +1892,7 @@ contract Halo2Verifier { // 64 KiB when this compact form is emitted. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } if q_has_top { mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) @@ -1554,6 +1904,7 @@ contract Halo2Verifier { case 0x06 { // The safety validator guarantees a spilled operand // exists before ADD. q_top is the right operand. + if eq(q_sp, 0x80e0) { q_program_fail() } q_sp := sub(q_sp, 0x20) q_top := addmod(mload(q_sp), q_top, r) } @@ -1590,6 +1941,7 @@ contract Halo2Verifier { // already range-checked Fr scalar in verifier memory. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_top := addmod(q_top, mload(q_ptr), r) } // VM 0x11 MUL_MEM_U16: multiply q_top by a short memory load. @@ -1597,6 +1949,7 @@ contract Halo2Verifier { // In-place multiply by a planned memory word. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_top := mulmod(q_top, mload(q_ptr), r) } // VM 0x13 ADD_MUL_CONST_U8_MEM_U16: fused q_top += mem * const. @@ -1606,6 +1959,7 @@ contract Halo2Verifier { let q_ptr := shr(240, q_word) let qconst := byte(2, q_word) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_top := addmod( q_top, mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), @@ -1650,6 +2004,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1687,6 +2042,7 @@ contract Halo2Verifier { // whole identity is gated by mload(q_cond_ptr). q_cond_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_cond_ptr, 0x2ca0), 0x4340) { q_program_fail() } } let q_acc := 0 @@ -1721,6 +2077,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1733,12 +2090,14 @@ contract Halo2Verifier { for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { let q_lhs := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + if gt(sub(q_lhs, 0x2ca0), 0x4340) { q_program_fail() } let q_lhs_value := mload(q_lhs) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { let q_word := mload(q_pc) let qconst := byte(0, q_word) let q_rhs := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_rhs, 0x2ca0), 0x4340) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -1758,6 +2117,8 @@ contract Halo2Verifier { let q_lhs_base := shr(240, q_pair_word) let q_rhs_base := and(shr(224, q_pair_word), 0xffff) q_pc := add(q_pc, 0x04) + if gt(sub(q_lhs_base, 0x2ca0), 0x4280) { q_program_fail() } + if gt(sub(q_rhs_base, 0x2ca0), 0x4280) { q_program_fail() } let q_coeff_pc := q_pc q_pc := add(q_pc, 13) for { let q_i := 0 } lt(q_i, 7) { q_i := add(q_i, 1) } { @@ -1783,6 +2144,7 @@ contract Halo2Verifier { let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + if gt(sub(q_ptr, 0x2ca0), 0x4340) { q_program_fail() } q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1797,6 +2159,8 @@ contract Halo2Verifier { let q_lhs := and(shr(232, q_word), 0xffff) let q_rhs := and(shr(216, q_word), 0xffff) q_pc := add(q_pc, 5) + if gt(sub(q_lhs, 0x2ca0), 0x4340) { q_program_fail() } + if gt(sub(q_rhs, 0x2ca0), 0x4340) { q_program_fail() } q_acc := addmod( q_acc, mulmod( @@ -1833,70 +2197,70 @@ contract Halo2Verifier { // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := 0x7160 + q_sp := 0x80e0 // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. { let delta := 0x8634d0aa021aaf843cab354fabb0062f6502437c6a09c006c083479590189d7 - let q_perm_vals := 0x7160 - let q_perm_sigmas := 0x7280 - let q_perm_z_cur := 0x73a0 - let q_perm_z_next := 0x7400 - let q_perm_z_last := 0x7460 - let q_perm_delta_base_ptr := 0x74a0 + let q_perm_vals := 0x80e0 + let q_perm_sigmas := 0x8200 + let q_perm_z_cur := 0x8320 + let q_perm_z_next := 0x8380 + let q_perm_z_last := 0x83e0 + let q_perm_delta_base_ptr := 0x8420 let q_perm_num_cols := 9 let q_perm_num_sets := 3 let q_perm_chunk_len := 3 let q_perm_delta_chunk := 0x4285088329c399ea457a8ca1d30f8957e74c7f529842a1579b4fee55b3982923 - mstore(add(q_perm_vals, 0x0), mload(0x5b20)) + mstore(add(q_perm_vals, 0x0), mload(0x6aa0)) { for { let q_perm_val_load_i := 0 } lt(q_perm_val_load_i, 5) { q_perm_val_load_i := add(q_perm_val_load_i, 1) } { let q_perm_val_load_dst_off := shl(5, q_perm_val_load_i) let q_perm_val_load_src_off := q_perm_val_load_dst_off - mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x5820, q_perm_val_load_src_off))) + mstore(add(add(q_perm_vals, 0x20), q_perm_val_load_dst_off), mload(add(0x67a0, q_perm_val_load_src_off))) } } - mstore(add(q_perm_vals, 0xc0), mload(0x5800)) + mstore(add(q_perm_vals, 0xc0), mload(0x6780)) mstore(add(q_perm_vals, 0xe0), mload(INSTANCE_EVAL_MPTR)) - mstore(add(q_perm_vals, 0x100), mload(0x5920)) + mstore(add(q_perm_vals, 0x100), mload(0x68a0)) { for { let q_perm_sigma_load_i := 0 } lt(q_perm_sigma_load_i, 9) { q_perm_sigma_load_i := add(q_perm_sigma_load_i, 1) } { let q_perm_sigma_load_dst_off := shl(5, q_perm_sigma_load_i) let q_perm_sigma_load_src_off := q_perm_sigma_load_dst_off - mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x5d60, q_perm_sigma_load_src_off))) + mstore(add(add(q_perm_sigmas, 0x0), q_perm_sigma_load_dst_off), mload(add(0x6ce0, q_perm_sigma_load_src_off))) } } { for { let q_perm_z_cur_load_i := 0 } lt(q_perm_z_cur_load_i, 3) { q_perm_z_cur_load_i := add(q_perm_z_cur_load_i, 1) } { let q_perm_z_cur_load_dst_off := shl(5, q_perm_z_cur_load_i) let q_perm_z_cur_load_src_off := mul(q_perm_z_cur_load_i, 0x60) - mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x5e80, q_perm_z_cur_load_src_off))) + mstore(add(add(q_perm_z_cur, 0x0), q_perm_z_cur_load_dst_off), mload(add(0x6e00, q_perm_z_cur_load_src_off))) } } { for { let q_perm_z_next_load_i := 0 } lt(q_perm_z_next_load_i, 3) { q_perm_z_next_load_i := add(q_perm_z_next_load_i, 1) } { let q_perm_z_next_load_dst_off := shl(5, q_perm_z_next_load_i) let q_perm_z_next_load_src_off := mul(q_perm_z_next_load_i, 0x60) - mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x5ea0, q_perm_z_next_load_src_off))) + mstore(add(add(q_perm_z_next, 0x0), q_perm_z_next_load_dst_off), mload(add(0x6e20, q_perm_z_next_load_src_off))) } } - mstore(add(q_perm_z_last, 0x0), mload(0x5ec0)) - mstore(add(q_perm_z_last, 0x20), mload(0x5f20)) + mstore(add(q_perm_z_last, 0x0), mload(0x6e40)) + mstore(add(q_perm_z_last, 0x20), mload(0x6ea0)) let q_perm_eval := 0 q_perm_eval := mulmod(mload(L_0_MPTR), addmod(1, sub(r, mload(q_perm_z_cur)), r), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_perm_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_perm_eval, r)) let q_perm_zn := mload(add(q_perm_z_cur, 0x40)) q_perm_eval := mulmod(mload(L_LAST_MPTR), addmod(mulmod(q_perm_zn, q_perm_zn, r), sub(r, q_perm_zn), r), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_perm_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_perm_eval, r)) for { let q_perm_i := 1 } lt(q_perm_i, 3) { q_perm_i := add(q_perm_i, 1) } { let q_perm_cur := mload(add(q_perm_z_cur, shl(5, q_perm_i))) let q_perm_prev := mload(add(q_perm_z_last, shl(5, sub(q_perm_i, 1)))) q_perm_eval := mulmod(mload(L_0_MPTR), addmod(q_perm_cur, sub(r, q_perm_prev), r), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_perm_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_perm_eval, r)) } mstore(q_perm_delta_base_ptr, mulmod(mload(BETA_MPTR), mload(X_MPTR), r)) for { let q_perm_set := 0 } lt(q_perm_set, 3) { q_perm_set := add(q_perm_set, 1) } { @@ -1915,8 +2279,8 @@ contract Halo2Verifier { q_perm_delta_pow := mulmod(q_perm_delta_pow, delta, r) } q_perm_eval := mulmod(addmod(1, sub(r, addmod(mload(L_LAST_MPTR), mload(L_BLIND_MPTR), r)), r), addmod(q_perm_left, sub(r, q_perm_right), r), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_perm_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_perm_eval, r)) mstore(q_perm_delta_base_ptr, mulmod(mload(q_perm_delta_base_ptr), q_perm_delta_chunk, r)) } } @@ -1937,13 +2301,13 @@ contract Halo2Verifier { // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := 0x7160 + q_sp := 0x80e0 // Generated LogUp code follows the same y-batch order // as the Rust identity stream. { - let q_lookup_f := 0x7160 - let q_lookup_prefix := 0x71a0 - let q_lookup_suffix := 0x71e0 + let q_lookup_f := 0x80e0 + let q_lookup_prefix := 0x8120 + let q_lookup_suffix := 0x8160 let q_lookup_l0 := mload(L_0_MPTR) let q_lookup_llast := mload(L_LAST_MPTR) let q_lookup_lblind := mload(L_BLIND_MPTR) @@ -1953,54 +2317,54 @@ contract Halo2Verifier { let q_lookup_theta := mload(THETA_MPTR) { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x5fc0), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x6f40), r) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } { - let f_10 := mload(0x5c60) + let f_10 := mload(0x6be0) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_10, r) - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_1, r) - let q_lookup_eval := addmod(mulmod(mload(0x5fa0), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x6f20), addmod(var1, q_lookup_beta, r), r), sub(r, 1), r) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x5fa0) - let f_19 := mload(0x5d20) - let f_11 := mload(0x5c80) + let q_lookup_sum_h := mload(0x6f20) + let f_19 := mload(0x6ca0) + let f_11 := mload(0x6c00) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_11, r) - let f_12 := mload(0x5ca0) + let f_12 := mload(0x6c20) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_12, r) let q_lookup_s_sum_h := mulmod(f_19, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x5fe0), sub(r, addmod(mload(0x5fc0), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x6f60), sub(r, addmod(mload(0x6f40), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var1, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x5f80), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x6f00), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } } { { - let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x6040), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + let q_lookup_eval := mulmod(q_lookup_lsum, mload(0x6fc0), r) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } { - let f_0 := mload(0x5be0) + let f_0 := mload(0x6b60) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_0, r) - let a_6 := mload(0x5940) + let a_6 := mload(0x68c0) let var1 := addmod(mulmod(var0, q_lookup_theta, r), a_6, r) - let a_0 := mload(0x5820) + let a_0 := mload(0x67a0) let var2 := addmod(mulmod(var1, q_lookup_theta, r), a_0, r) mstore(add(q_lookup_f, 0x0), addmod(var2, q_lookup_beta, r)) - let f_1 := mload(0x5c00) + let f_1 := mload(0x6b80) let var3 := addmod(mulmod(0, q_lookup_theta, r), f_1, r) - let a_7 := mload(0x5960) + let a_7 := mload(0x68e0) let var4 := addmod(mulmod(var3, q_lookup_theta, r), a_7, r) - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var5 := addmod(mulmod(var4, q_lookup_theta, r), a_1, r) mstore(add(q_lookup_f, 0x20), addmod(var5, q_lookup_beta, r)) let q_lookup_product := 1 @@ -2021,26 +2385,26 @@ contract Halo2Verifier { for { let q_lookup_sum_i := 0 } lt(q_lookup_sum_i, 2) { q_lookup_sum_i := add(q_lookup_sum_i, 1) } { q_lookup_sum := addmod(q_lookup_sum, mulmod(mload(add(q_lookup_prefix, shl(5, q_lookup_sum_i))), mload(add(q_lookup_suffix, shl(5, q_lookup_sum_i))), r), r) } - let q_lookup_eval := addmod(mulmod(mload(0x6020), q_lookup_product, r), sub(r, q_lookup_sum), r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + let q_lookup_eval := addmod(mulmod(mload(0x6fa0), q_lookup_product, r), sub(r, q_lookup_sum), r) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } { - let q_lookup_sum_h := mload(0x6020) - let f_20 := mload(0x5d40) - let f_13 := mload(0x5cc0) + let q_lookup_sum_h := mload(0x6fa0) + let f_20 := mload(0x6cc0) + let f_13 := mload(0x6c40) let var0 := addmod(mulmod(0, q_lookup_theta, r), f_13, r) - let f_14 := mload(0x5ce0) + let f_14 := mload(0x6c60) let var1 := addmod(mulmod(var0, q_lookup_theta, r), f_14, r) - let f_15 := mload(0x5d00) + let f_15 := mload(0x6c80) let var2 := addmod(mulmod(var1, q_lookup_theta, r), f_15, r) let q_lookup_s_sum_h := mulmod(f_20, q_lookup_sum_h, r) - let q_lookup_diff := addmod(mload(0x6060), sub(r, addmod(mload(0x6040), q_lookup_s_sum_h, r)), r) + let q_lookup_diff := addmod(mload(0x6fe0), sub(r, addmod(mload(0x6fc0), q_lookup_s_sum_h, r)), r) let q_lookup_t_beta := addmod(var2, q_lookup_beta, r) - let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x6000), r) + let q_lookup_core := addmod(mulmod(q_lookup_diff, q_lookup_t_beta, r), mload(0x6f80), r) let q_lookup_eval := mulmod(q_lookup_active, q_lookup_core, r) - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) - mstore(0x6cc0, addmod(mload(0x6cc0), q_lookup_eval, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) + mstore(0x7c40, addmod(mload(0x7c40), q_lookup_eval, r)) } } } @@ -2061,100 +2425,100 @@ contract Halo2Verifier { // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := 0x7160 + q_sp := 0x80e0 // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx case 0 { { let var0 := 0x1 - let a_3_prev_1 := mload(0x5980) - let a_4_prev_1 := mload(0x59a0) + let a_3_prev_1 := mload(0x6900) + let a_4_prev_1 := mload(0x6920) let var1 := addmod(a_3_prev_1, a_4_prev_1, r) - let a_3 := mload(0x5880) + let a_3 := mload(0x6800) let var2 := addmod(var1, a_3, r) let var3 := 0x40000000000 - let a_1_prev_1 := mload(0x59e0) + let a_1_prev_1 := mload(0x6960) let var4 := mulmod(var3, a_1_prev_1, r) let var5 := 0x100000 - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var6 := mulmod(var5, a_1, r) let var7 := addmod(var4, var6, r) - let a_1_next_1 := mload(0x58e0) + let a_1_next_1 := mload(0x6860) let var8 := addmod(var7, a_1_next_1, r) let var9 := 0x2 - let a_0_prev_1 := mload(0x59c0) + let a_0_prev_1 := mload(0x6940) let var10 := mulmod(var3, a_0_prev_1, r) - let a_0 := mload(0x5820) + let a_0 := mload(0x67a0) let var11 := mulmod(var5, a_0, r) let var12 := addmod(var10, var11, r) - let a_0_next_1 := mload(0x58c0) + let a_0_next_1 := mload(0x6840) let var13 := addmod(var12, a_0_next_1, r) let var14 := mulmod(var9, var13, r) let var15 := addmod(var8, var14, r) let var16 := addmod(0, sub(r, var15), r) let var17 := addmod(var2, var16, r) let var18 := mulmod(var0, var17, r) - mstore(0x7160, var18) + mstore(0x80e0, var18) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x60) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } } case 1 { { let var0 := 0x1 - let a_3_prev_1 := mload(0x5980) - let a_4_prev_1 := mload(0x59a0) + let a_3_prev_1 := mload(0x6900) + let a_4_prev_1 := mload(0x6920) let var1 := addmod(a_3_prev_1, a_4_prev_1, r) let var2 := 0x40000000000 - let a_1_prev_1 := mload(0x59e0) + let a_1_prev_1 := mload(0x6960) let var3 := mulmod(var2, a_1_prev_1, r) let var4 := 0x100000 - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var5 := mulmod(var4, a_1, r) let var6 := addmod(var3, var5, r) - let a_1_next_1 := mload(0x58e0) + let a_1_next_1 := mload(0x6860) let var7 := addmod(var6, a_1_next_1, r) let var8 := 0x2 - let a_0_prev_1 := mload(0x59c0) + let a_0_prev_1 := mload(0x6940) let var9 := mulmod(var2, a_0_prev_1, r) - let a_0 := mload(0x5820) + let a_0 := mload(0x67a0) let var10 := mulmod(var4, a_0, r) let var11 := addmod(var9, var10, r) - let a_0_next_1 := mload(0x58c0) + let a_0_next_1 := mload(0x6840) let var12 := addmod(var11, a_0_next_1, r) let var13 := mulmod(var8, var12, r) let var14 := addmod(var7, var13, r) let var15 := addmod(0, sub(r, var14), r) let var16 := addmod(var1, var15, r) let var17 := mulmod(var0, var16, r) - mstore(0x7160, var17) + mstore(0x80e0, var17) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0x80) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } } case 2 { { let var0 := 0x1 let var1 := 0x1000000000000000 - let a_4 := mload(0x58a0) + let a_4 := mload(0x6820) let var2 := mulmod(var1, a_4, r) let var3 := 0x10000000000 - let a_3_prev_1 := mload(0x5980) + let a_3_prev_1 := mload(0x6900) let var4 := mulmod(var3, a_3_prev_1, r) let var5 := addmod(var2, var4, r) let var6 := 0x400000 - let a_4_prev_1 := mload(0x59a0) + let a_4_prev_1 := mload(0x6920) let var7 := mulmod(var6, a_4_prev_1, r) let var8 := addmod(var5, var7, r) - let a_3 := mload(0x5880) + let a_3 := mload(0x6800) let var9 := addmod(var8, a_3, r) let var10 := 0x40000000000 let var11 := mulmod(var10, a_3, r) @@ -2176,54 +2540,54 @@ contract Halo2Verifier { let var27 := addmod(var24, var26, r) let var28 := addmod(var27, a_3_prev_1, r) let var29 := addmod(var19, var28, r) - let a_0_prev_1 := mload(0x59c0) + let a_0_prev_1 := mload(0x6940) let var30 := mulmod(var10, a_0_prev_1, r) - let a_0 := mload(0x5820) + let a_0 := mload(0x67a0) let var31 := mulmod(var25, a_0, r) let var32 := addmod(var30, var31, r) - let a_0_next_1 := mload(0x58c0) + let a_0_next_1 := mload(0x6840) let var33 := addmod(var32, a_0_next_1, r) let var34 := 0x2 - let a_1_prev_1 := mload(0x59e0) + let a_1_prev_1 := mload(0x6960) let var35 := mulmod(var10, a_1_prev_1, r) - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var36 := mulmod(var25, a_1, r) let var37 := addmod(var35, var36, r) - let a_1_next_1 := mload(0x58e0) + let a_1_next_1 := mload(0x6860) let var38 := addmod(var37, a_1_next_1, r) let var39 := mulmod(var34, var38, r) let var40 := addmod(var33, var39, r) let var41 := addmod(0, sub(r, var40), r) let var42 := addmod(var29, var41, r) let var43 := mulmod(var0, var42, r) - mstore(0x7160, var43) + mstore(0x80e0, var43) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xa0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } } case 3 { { let var0 := 0x1 let var1 := 0x10000000000000 - let a_3_next_1 := mload(0x5a00) + let a_3_next_1 := mload(0x6980) let var2 := mulmod(var1, a_3_next_1, r) let var3 := 0x4000000000 - let a_3_prev_1 := mload(0x5980) + let a_3_prev_1 := mload(0x6900) let var4 := mulmod(var3, a_3_prev_1, r) let var5 := addmod(var2, var4, r) let var6 := 0x4000 - let a_4_prev_1 := mload(0x59a0) + let a_4_prev_1 := mload(0x6920) let var7 := mulmod(var6, a_4_prev_1, r) let var8 := addmod(var5, var7, r) let var9 := 0x400 - let a_3 := mload(0x5880) + let a_3 := mload(0x6800) let var10 := mulmod(var9, a_3, r) let var11 := addmod(var8, var10, r) - let a_4 := mload(0x58a0) + let a_4 := mload(0x6820) let var12 := addmod(var11, a_4, r) let var13 := 0x40000000000000 let var14 := mulmod(var13, a_4, r) @@ -2250,37 +2614,37 @@ contract Halo2Verifier { let var35 := addmod(var33, var34, r) let var36 := addmod(var35, a_3_prev_1, r) let var37 := addmod(var25, var36, r) - let a_0_prev_1 := mload(0x59c0) + let a_0_prev_1 := mload(0x6940) let var38 := mulmod(var15, a_0_prev_1, r) let var39 := 0x100000 - let a_0 := mload(0x5820) + let a_0 := mload(0x67a0) let var40 := mulmod(var39, a_0, r) let var41 := addmod(var38, var40, r) - let a_0_next_1 := mload(0x58c0) + let a_0_next_1 := mload(0x6840) let var42 := addmod(var41, a_0_next_1, r) let var43 := 0x2 - let a_1_prev_1 := mload(0x59e0) + let a_1_prev_1 := mload(0x6960) let var44 := mulmod(var15, a_1_prev_1, r) - let a_1 := mload(0x5840) + let a_1 := mload(0x67c0) let var45 := mulmod(var39, a_1, r) let var46 := addmod(var44, var45, r) - let a_1_next_1 := mload(0x58e0) + let a_1_next_1 := mload(0x6860) let var47 := addmod(var46, a_1_next_1, r) let var48 := mulmod(var43, var47, r) let var49 := addmod(var42, var48, r) let var50 := addmod(0, sub(r, var49), r) let var51 := addmod(var37, var50, r) let var52 := mulmod(var0, var51, r) - mstore(0x7160, var52) + mstore(0x80e0, var52) } - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) { let q_selector_ptr := add(SELECTOR_ACC_MPTR, 0xc0) let q_selector_acc := mload(q_selector_ptr) - mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x7160), r)) + mstore(q_selector_ptr, addmod(q_selector_acc, mload(0x80e0), r)) } } - default { revert(0, 0) } + default { q_program_fail() } } // VM 0x0b FOLD_SELECTOR: consume q_top into one simple-selector bucket. case 0x0b { @@ -2292,6 +2656,11 @@ contract Halo2Verifier { q_pc := add(q_pc, 3) let q_sel_idx := shr(16, q_selector_payload) let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, 14)) { q_program_fail() } + if gt(q_sel_gap, 0x22) { q_program_fail() } let q_eval := q_top q_has_top := 0 // Simple-selector identity: keep the same y-batch @@ -2301,28 +2670,34 @@ contract Halo2Verifier { // The global fully-evaluated accumulator is still // multiplied by y so later main identities land at the // same y powers as Rust's reverse fold. - mstore(0x6cc0, mulmod(mload(0x6cc0), y, r)) + mstore(0x7c40, mulmod(mload(0x7c40), y, r)) let q_target_ptr := add(SELECTOR_ACC_MPTR, shl(5, q_sel_idx)) let q_sel_acc := mload(q_target_ptr) if q_sel_gap { // Selector buckets are sparse in the global // identity stream. Precomputed y^gap advances only // this selector's local accumulator. - q_sel_acc := mulmod(q_sel_acc, mload(add(0x6d00, shl(5, q_sel_gap))), r) + q_sel_acc := mulmod(q_sel_acc, mload(add(0x7c80, shl(5, q_sel_gap))), r) } mstore(q_target_ptr, addmod(q_sel_acc, q_eval, r)) } // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, 0x80e0)) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled @@ -2340,65 +2715,65 @@ contract Halo2Verifier { // selector commitment in the linearized MSM. { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x00) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0440)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0440)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x20) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0420)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0420)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x40) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x03c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x03c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x60) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x03a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x03a0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x80) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0360)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0360)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xa0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0340)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0340)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xc0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0320)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0320)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0xe0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0300)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0300)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0100) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x02e0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x02e0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0120) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x02c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x02c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0140) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0280)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0280)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0160) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x0240)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x0240)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x0180) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x01c0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x01c0)), r)) } { let q_sel_ptr := add(SELECTOR_ACC_MPTR, 0x01a0) - mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x6d00, 0x01a0)), r)) + mstore(q_sel_ptr, mulmod(mload(q_sel_ptr), mload(add(0x7c80, 0x01a0)), r)) } // Fully evaluated identities are the constant-polynomial side // of the linearization query. Rust subtracts that grouped // scalar into expected_eval, so Solidity stores -nu_y(x). - let linearization_expected_eval := addmod(0, sub(r, mload(0x6cc0)), r) + let linearization_expected_eval := addmod(0, sub(r, mload(0x7c40)), r) mstore(QUOTIENT_EVAL_MPTR, linearization_expected_eval) pop(y) } @@ -2499,44 +2874,44 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[0]: 33 commitment(s) (rolled, m>=4) + // q_eval_set[0]: 33 evaluation term(s), 32 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x6cc0, 0x5800) - mstore(0x6ce0, 0x5f80) - mstore(0x6d00, 0x5fa0) - mstore(0x6d20, 0x6000) - mstore(0x6d40, 0x6020) - mstore(0x6d60, 0x5b20) - mstore(0x6d80, 0x5b40) - mstore(0x6da0, 0x5b60) - mstore(0x6dc0, 0x5b80) - mstore(0x6de0, 0x5ba0) - mstore(0x6e00, 0x5bc0) - mstore(0x6e20, 0x5be0) - mstore(0x6e40, 0x5c00) - mstore(0x6e60, 0x5c20) - mstore(0x6e80, 0x5c40) - mstore(0x6ea0, 0x5c60) - mstore(0x6ec0, 0x5c80) - mstore(0x6ee0, 0x5ca0) - mstore(0x6f00, 0x5cc0) - mstore(0x6f20, 0x5ce0) - mstore(0x6f40, 0x5d00) - mstore(0x6f60, 0x5d20) - mstore(0x6f80, 0x5d40) - mstore(0x6fa0, 0x5d60) - mstore(0x6fc0, 0x5d80) - mstore(0x6fe0, 0x5da0) - mstore(0x7000, 0x5dc0) - mstore(0x7020, 0x5de0) - mstore(0x7040, 0x5e00) - mstore(0x7060, 0x5e20) - mstore(0x7080, 0x5e40) - mstore(0x70a0, 0x5e60) - mstore(0x70c0, QUOTIENT_EVAL_MPTR) - let q_eval_set_0 := mload(0x5800) + mstore(0x7c40, 0x6780) + mstore(0x7c60, 0x6f00) + mstore(0x7c80, 0x6f20) + mstore(0x7ca0, 0x6f80) + mstore(0x7cc0, 0x6fa0) + mstore(0x7ce0, 0x6aa0) + mstore(0x7d00, 0x6ac0) + mstore(0x7d20, 0x6ae0) + mstore(0x7d40, 0x6b00) + mstore(0x7d60, 0x6b20) + mstore(0x7d80, 0x6b40) + mstore(0x7da0, 0x6b60) + mstore(0x7dc0, 0x6b80) + mstore(0x7de0, 0x6ba0) + mstore(0x7e00, 0x6bc0) + mstore(0x7e20, 0x6be0) + mstore(0x7e40, 0x6c00) + mstore(0x7e60, 0x6c20) + mstore(0x7e80, 0x6c40) + mstore(0x7ea0, 0x6c60) + mstore(0x7ec0, 0x6c80) + mstore(0x7ee0, 0x6ca0) + mstore(0x7f00, 0x6cc0) + mstore(0x7f20, 0x6ce0) + mstore(0x7f40, 0x6d00) + mstore(0x7f60, 0x6d20) + mstore(0x7f80, 0x6d40) + mstore(0x7fa0, 0x6d60) + mstore(0x7fc0, 0x6d80) + mstore(0x7fe0, 0x6da0) + mstore(0x8000, 0x6dc0) + mstore(0x8020, 0x6de0) + mstore(0x8040, QUOTIENT_EVAL_MPTR) + let q_eval_set_0 := mload(0x6780) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x6cc0, 0x20) + let eval_p := add(0x7c40, 0x20) for { let i := 1 } lt(i, 0x21) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2549,13 +2924,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[1]: 3 commitment(s) - let q_eval_set_0 := mload(0x5f40) - let q_eval_set_1 := mload(0x5f60) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x5fc0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x5fe0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6040), mload(add(X1_POWERS_MPTR, 0x40)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6060), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + // q_eval_set[1]: 3 evaluation term(s), 3 commitment term(s) + let q_eval_set_0 := mload(0x6ec0) + let q_eval_set_1 := mload(0x6ee0) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6f40), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6f60), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6fc0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6fe0), mload(add(X1_POWERS_MPTR, 0x40)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0x20), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0x40), q_eval_set_1) } @@ -2563,37 +2938,37 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[2]: 8 commitment(s) (rolled, m>=4) + // q_eval_set[2]: 8 evaluation term(s), 8 commitment term(s) (rolled, m>=4) // stage per-(commit, rotation) eval source addresses - mstore(0x6cc0, 0x5820) - mstore(0x6ce0, 0x58c0) - mstore(0x6d00, 0x59c0) - mstore(0x6d20, 0x5840) - mstore(0x6d40, 0x58e0) - mstore(0x6d60, 0x59e0) - mstore(0x6d80, 0x5860) - mstore(0x6da0, 0x5900) - mstore(0x6dc0, 0x5a80) - mstore(0x6de0, 0x5880) - mstore(0x6e00, 0x5a00) - mstore(0x6e20, 0x5980) - mstore(0x6e40, 0x58a0) - mstore(0x6e60, 0x5a20) - mstore(0x6e80, 0x59a0) - mstore(0x6ea0, 0x5920) - mstore(0x6ec0, 0x5ae0) - mstore(0x6ee0, 0x5ac0) - mstore(0x6f00, 0x5940) - mstore(0x6f20, 0x5a60) - mstore(0x6f40, 0x5a40) - mstore(0x6f60, 0x5960) - mstore(0x6f80, 0x5b00) - mstore(0x6fa0, 0x5aa0) - let q_eval_set_0 := mload(0x5820) - let q_eval_set_1 := mload(0x58c0) - let q_eval_set_2 := mload(0x59c0) + mstore(0x7c40, 0x67a0) + mstore(0x7c60, 0x6840) + mstore(0x7c80, 0x6940) + mstore(0x7ca0, 0x67c0) + mstore(0x7cc0, 0x6860) + mstore(0x7ce0, 0x6960) + mstore(0x7d00, 0x67e0) + mstore(0x7d20, 0x6880) + mstore(0x7d40, 0x6a00) + mstore(0x7d60, 0x6800) + mstore(0x7d80, 0x6980) + mstore(0x7da0, 0x6900) + mstore(0x7dc0, 0x6820) + mstore(0x7de0, 0x69a0) + mstore(0x7e00, 0x6920) + mstore(0x7e20, 0x68a0) + mstore(0x7e40, 0x6a60) + mstore(0x7e60, 0x6a40) + mstore(0x7e80, 0x68c0) + mstore(0x7ea0, 0x69e0) + mstore(0x7ec0, 0x69c0) + mstore(0x7ee0, 0x68e0) + mstore(0x7f00, 0x6a80) + mstore(0x7f20, 0x6a20) + let q_eval_set_0 := mload(0x67a0) + let q_eval_set_1 := mload(0x6840) + let q_eval_set_2 := mload(0x6940) let pow_p := add(X1_POWERS_MPTR, 0x20) - let eval_p := add(0x6cc0, 0x60) + let eval_p := add(0x7c40, 0x60) for { let i := 1 } lt(i, 0x8) { i := add(i, 1) } { let pow := mload(pow_p) q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(mload(eval_p)), pow, r), r) @@ -2610,13 +2985,13 @@ contract Halo2Verifier { // emitted by the multi-prepare lowering pass and are kept // grouped so gas checkpoints can attribute their cost. { - // q_eval_set[3]: 2 commitment(s) - let q_eval_set_0 := mload(0x5e80) - let q_eval_set_1 := mload(0x5ea0) - let q_eval_set_2 := mload(0x5ec0) - q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x5ee0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x5f00), mload(add(X1_POWERS_MPTR, 0x20)), r), r) - q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x5f20), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + // q_eval_set[3]: 2 evaluation term(s), 2 commitment term(s) + let q_eval_set_0 := mload(0x6e00) + let q_eval_set_1 := mload(0x6e20) + let q_eval_set_2 := mload(0x6e40) + q_eval_set_0 := addmod(q_eval_set_0, mulmod(mload(0x6e60), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_1 := addmod(q_eval_set_1, mulmod(mload(0x6e80), mload(add(X1_POWERS_MPTR, 0x20)), r), r) + q_eval_set_2 := addmod(q_eval_set_2, mulmod(mload(0x6ea0), mload(add(X1_POWERS_MPTR, 0x20)), r), r) mstore(add(Q_EVAL_SET_MPTR, 0xc0), q_eval_set_0) mstore(add(Q_EVAL_SET_MPTR, 0xe0), q_eval_set_1) mstore(add(Q_EVAL_SET_MPTR, 0x100), q_eval_set_2) @@ -2784,139 +3159,140 @@ contract Halo2Verifier { v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x40)), x4_pow_2, r), r) v := addmod(v, mulmod(calldataload(add(Q_EVAL_CPTR, 0x60)), x4_pow_3, r), r) v := addmod(v, mulmod(mload(F_EVAL_MPTR), x4_pow_4, r), r) - mcopy(0x6cc0, 0x6480, 0x80) - mstore(0x6d40, mload(add(X1_POWERS_MPTR, 0x20))) - mcopy(0x6d60, 0x6700, 0x80) - mstore(0x6de0, mload(add(X1_POWERS_MPTR, 0x40))) - mcopy(0x6e00, 0x6500, 0x80) - mstore(0x6e80, mload(add(X1_POWERS_MPTR, 0x60))) - mcopy(0x6ea0, 0x6780, 0x80) - mstore(0x6f20, mload(add(X1_POWERS_MPTR, 0x80))) - mcopy(0x6f40, 0x2c80, 0x80) - mstore(0x6fc0, mload(add(X1_POWERS_MPTR, 0xa0))) - mcopy(0x6fe0, 0x2a00, 0x80) - mstore(0x7060, mload(add(X1_POWERS_MPTR, 0xc0))) - mcopy(0x7080, 0x2a80, 0x80) - mstore(0x7100, mload(add(X1_POWERS_MPTR, 0xe0))) - mcopy(0x7120, 0x2b00, 0x80) - mstore(0x71a0, mload(add(X1_POWERS_MPTR, 0x100))) - mcopy(0x71c0, 0x2b80, 0x80) - mstore(0x7240, mload(add(X1_POWERS_MPTR, 0x120))) - mcopy(0x7260, 0x2c00, 0x80) - mstore(0x72e0, mload(add(X1_POWERS_MPTR, 0x140))) - mcopy(0x7300, 0x2800, 0x80) - mstore(0x7380, mload(add(X1_POWERS_MPTR, 0x160))) - mcopy(0x73a0, 0x2880, 0x80) - mstore(0x7420, mload(add(X1_POWERS_MPTR, 0x180))) - mcopy(0x7440, 0x2900, 0x80) - mstore(0x74c0, mload(add(X1_POWERS_MPTR, 0x1a0))) - mcopy(0x74e0, 0x2980, 0x80) - mstore(0x7560, mload(add(X1_POWERS_MPTR, 0x1c0))) - mcopy(0x7580, 0x2d00, 0x80) - mstore(0x7600, mload(add(X1_POWERS_MPTR, 0x1e0))) - mcopy(0x7620, 0x2d80, 0x80) - mstore(0x76a0, mload(add(X1_POWERS_MPTR, 0x200))) - mcopy(0x76c0, 0x2e00, 0x80) - mstore(0x7740, mload(add(X1_POWERS_MPTR, 0x220))) - mcopy(0x7760, 0x2e80, 0x80) - mstore(0x77e0, mload(add(X1_POWERS_MPTR, 0x240))) - mcopy(0x7800, 0x2f00, 0x80) - mstore(0x7880, mload(add(X1_POWERS_MPTR, 0x260))) - mcopy(0x78a0, 0x2f80, 0x80) - mstore(0x7920, mload(add(X1_POWERS_MPTR, 0x280))) - mcopy(0x7940, 0x3180, 0x80) - mstore(0x79c0, mload(add(X1_POWERS_MPTR, 0x2a0))) - mcopy(0x79e0, 0x3200, 0x80) - mstore(0x7a60, mload(add(X1_POWERS_MPTR, 0x2c0))) - mcopy(0x7a80, 0x3800, 0x80) - mstore(0x7b00, mload(add(X1_POWERS_MPTR, 0x2e0))) - mcopy(0x7b20, 0x3880, 0x80) - mstore(0x7ba0, mload(add(X1_POWERS_MPTR, 0x300))) - mcopy(0x7bc0, 0x3900, 0x80) - mstore(0x7c40, mload(add(X1_POWERS_MPTR, 0x320))) - mcopy(0x7c60, 0x3980, 0x80) - mstore(0x7ce0, mload(add(X1_POWERS_MPTR, 0x340))) - mcopy(0x7d00, 0x3a00, 0x80) - mstore(0x7d80, mload(add(X1_POWERS_MPTR, 0x360))) - mcopy(0x7da0, 0x3a80, 0x80) - mstore(0x7e20, mload(add(X1_POWERS_MPTR, 0x380))) - mcopy(0x7e40, 0x3b00, 0x80) - mstore(0x7ec0, mload(add(X1_POWERS_MPTR, 0x3a0))) - mcopy(0x7ee0, 0x3b80, 0x80) - mstore(0x7f60, mload(add(X1_POWERS_MPTR, 0x3c0))) - mcopy(0x7f80, 0x3c00, 0x80) - mstore(0x8000, mload(add(X1_POWERS_MPTR, 0x3e0))) + mcopy(0x7c40, 0x7400, 0x80) + mstore(0x7cc0, mload(add(X1_POWERS_MPTR, 0x20))) + mcopy(0x7ce0, 0x7680, 0x80) + mstore(0x7d60, mload(add(X1_POWERS_MPTR, 0x40))) + mcopy(0x7d80, 0x7480, 0x80) + mstore(0x7e00, mload(add(X1_POWERS_MPTR, 0x60))) + mcopy(0x7e20, 0x7700, 0x80) + mstore(0x7ea0, mload(add(X1_POWERS_MPTR, 0x80))) + mcopy(0x7ec0, 0x3c00, 0x80) + mstore(0x7f40, mload(add(X1_POWERS_MPTR, 0xa0))) + mcopy(0x7f60, 0x3980, 0x80) + mstore(0x7fe0, mload(add(X1_POWERS_MPTR, 0xc0))) + mcopy(0x8000, 0x3a00, 0x80) + mstore(0x8080, mload(add(X1_POWERS_MPTR, 0xe0))) + mcopy(0x80a0, 0x3a80, 0x80) + mstore(0x8120, mload(add(X1_POWERS_MPTR, 0x100))) + mcopy(0x8140, 0x3b00, 0x80) + mstore(0x81c0, mload(add(X1_POWERS_MPTR, 0x120))) + mcopy(0x81e0, 0x3b80, 0x80) + mstore(0x8260, mload(add(X1_POWERS_MPTR, 0x140))) + mcopy(0x8280, 0x3780, 0x80) + mstore(0x8300, mload(add(X1_POWERS_MPTR, 0x160))) + mcopy(0x8320, 0x3800, 0x80) + mstore(0x83a0, mload(add(X1_POWERS_MPTR, 0x180))) + mcopy(0x83c0, 0x3880, 0x80) + mstore(0x8440, mload(add(X1_POWERS_MPTR, 0x1a0))) + mcopy(0x8460, 0x3900, 0x80) + mstore(0x84e0, mload(add(X1_POWERS_MPTR, 0x1c0))) + mcopy(0x8500, 0x3c80, 0x80) + mstore(0x8580, mload(add(X1_POWERS_MPTR, 0x1e0))) + mcopy(0x85a0, 0x3d00, 0x80) + mstore(0x8620, mload(add(X1_POWERS_MPTR, 0x200))) + mcopy(0x8640, 0x3d80, 0x80) + mstore(0x86c0, mload(add(X1_POWERS_MPTR, 0x220))) + mcopy(0x86e0, 0x3e00, 0x80) + mstore(0x8760, mload(add(X1_POWERS_MPTR, 0x240))) + mcopy(0x8780, 0x3e80, 0x80) + mstore(0x8800, mload(add(X1_POWERS_MPTR, 0x260))) + mcopy(0x8820, 0x3f00, 0x80) + mstore(0x88a0, mload(add(X1_POWERS_MPTR, 0x280))) + mcopy(0x88c0, 0x4100, 0x80) + mstore(0x8940, mload(add(X1_POWERS_MPTR, 0x2a0))) + mcopy(0x8960, 0x4180, 0x80) + mstore(0x89e0, mload(add(X1_POWERS_MPTR, 0x2c0))) + mcopy(0x8a00, 0x4780, 0x80) + mstore(0x8a80, mload(add(X1_POWERS_MPTR, 0x2e0))) + mcopy(0x8aa0, 0x4800, 0x80) + mstore(0x8b20, mload(add(X1_POWERS_MPTR, 0x300))) + mcopy(0x8b40, 0x4880, 0x80) + mstore(0x8bc0, mload(add(X1_POWERS_MPTR, 0x320))) + mcopy(0x8be0, 0x4900, 0x80) + mstore(0x8c60, mload(add(X1_POWERS_MPTR, 0x340))) + mcopy(0x8c80, 0x4980, 0x80) + mstore(0x8d00, mload(add(X1_POWERS_MPTR, 0x360))) + mcopy(0x8d20, 0x4a00, 0x80) + mstore(0x8da0, mload(add(X1_POWERS_MPTR, 0x380))) + mcopy(0x8dc0, 0x4a80, 0x80) + mstore(0x8e40, mload(add(X1_POWERS_MPTR, 0x3a0))) + mcopy(0x8e60, 0x4b00, 0x80) + mstore(0x8ee0, mload(add(X1_POWERS_MPTR, 0x3c0))) + mcopy(0x8f00, 0x4b80, 0x80) + mstore(0x8f80, mload(add(X1_POWERS_MPTR, 0x3e0))) let lin_query_scalar_31 := mload(add(X1_POWERS_MPTR, 0x400)) let lin_cur_scalar_31 := mulmod(lin_query_scalar_31, lin_one_minus_x_n, r) - mcopy(0x8020, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) - mstore(0x80a0, lin_cur_scalar_31) + mcopy(0x8fa0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x0), 0x80) + mstore(0x9020, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x80c0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) - mstore(0x8140, lin_cur_scalar_31) + mcopy(0x9040, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x80), 0x80) + mstore(0x90c0, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x8160, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) - mstore(0x81e0, lin_cur_scalar_31) + mcopy(0x90e0, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x100), 0x80) + mstore(0x9160, lin_cur_scalar_31) lin_cur_scalar_31 := mulmod(lin_cur_scalar_31, lin_x_split, r) - mcopy(0x8200, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) - mstore(0x8280, lin_cur_scalar_31) - mcopy(0x82a0, 0x3000, 0x80) - mstore(0x8320, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) - mcopy(0x8340, 0x3080, 0x80) - mstore(0x83c0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) - mcopy(0x83e0, 0x3100, 0x80) - mstore(0x8460, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) - mcopy(0x8480, 0x3280, 0x80) - mstore(0x8500, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) - mcopy(0x8520, 0x3300, 0x80) - mstore(0x85a0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) - mcopy(0x85c0, 0x3380, 0x80) - mstore(0x8640, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) - mcopy(0x8660, 0x3400, 0x80) - mstore(0x86e0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) - mcopy(0x8700, 0x3480, 0x80) - mstore(0x8780, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) - mcopy(0x87a0, 0x3500, 0x80) - mstore(0x8820, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) - mcopy(0x8840, 0x3580, 0x80) - mstore(0x88c0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) - mcopy(0x88e0, 0x3600, 0x80) - mstore(0x8960, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x140)), r)) - mcopy(0x8980, 0x3680, 0x80) - mstore(0x8a00, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x160)), r)) - mcopy(0x8a20, 0x3700, 0x80) - mstore(0x8aa0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x180)), r)) - mcopy(0x8ac0, 0x3780, 0x80) - mstore(0x8b40, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x1a0)), r)) - mcopy(0x8b60, 0x6680, 0x80) - mstore(0x8be0, x4_pow_1) - mcopy(0x8c00, 0x6800, 0x80) - mstore(0x8c80, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) - mcopy(0x8ca0, 0x6880, 0x80) - mstore(0x8d20, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) - mcopy(0x8d40, 0x6080, 0x80) - mstore(0x8dc0, x4_pow_2) - mcopy(0x8de0, 0x6100, 0x80) - mstore(0x8e60, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) - mcopy(0x8e80, 0x6180, 0x80) - mstore(0x8f00, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) - mcopy(0x8f20, 0x6200, 0x80) - mstore(0x8fa0, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_2, r)) - mcopy(0x8fc0, 0x6280, 0x80) - mstore(0x9040, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_2, r)) - mcopy(0x9060, 0x6300, 0x80) - mstore(0x90e0, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_2, r)) - mcopy(0x9100, 0x6380, 0x80) - mstore(0x9180, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_2, r)) - mcopy(0x91a0, 0x6400, 0x80) - mstore(0x9220, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_2, r)) - mcopy(0x9240, 0x6580, 0x80) - mstore(0x92c0, x4_pow_3) - mcopy(0x92e0, 0x6600, 0x80) - mstore(0x9360, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) - mcopy(0x9380, F_COM_MPTR, 0x80) - mstore(0x9400, x4_pow_4) + mcopy(0x9180, add(QUOTIENT_LIMB_COMMS_MPTR_BASE, 0x180), 0x80) + mstore(0x9200, lin_cur_scalar_31) + mcopy(0x9220, 0x3f80, 0x80) + mstore(0x92a0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x0)), r)) + mcopy(0x92c0, 0x4000, 0x80) + mstore(0x9340, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x20)), r)) + mcopy(0x9360, 0x4080, 0x80) + mstore(0x93e0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x40)), r)) + mcopy(0x9400, 0x4200, 0x80) + mstore(0x9480, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x60)), r)) + mcopy(0x94a0, 0x4280, 0x80) + mstore(0x9520, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x80)), r)) + mcopy(0x9540, 0x4300, 0x80) + mstore(0x95c0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xa0)), r)) + mcopy(0x95e0, 0x4380, 0x80) + mstore(0x9660, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xc0)), r)) + mcopy(0x9680, 0x4400, 0x80) + mstore(0x9700, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0xe0)), r)) + mcopy(0x9720, 0x4480, 0x80) + mstore(0x97a0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x100)), r)) + mcopy(0x97c0, 0x4500, 0x80) + mstore(0x9840, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x120)), r)) + mcopy(0x9860, 0x4580, 0x80) + mstore(0x98e0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x140)), r)) + mcopy(0x9900, 0x4600, 0x80) + mstore(0x9980, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x160)), r)) + mcopy(0x99a0, 0x4680, 0x80) + mstore(0x9a20, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x180)), r)) + mcopy(0x9a40, 0x4700, 0x80) + mstore(0x9ac0, mulmod(lin_query_scalar_31, mload(add(SELECTOR_ACC_MPTR, 0x1a0)), r)) + mcopy(0x9ae0, 0x7600, 0x80) + mstore(0x9b60, x4_pow_1) + mcopy(0x9b80, 0x7780, 0x80) + mstore(0x9c00, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_1, r)) + mcopy(0x9c20, 0x7800, 0x80) + mstore(0x9ca0, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_1, r)) + mcopy(0x9cc0, 0x7000, 0x80) + mstore(0x9d40, x4_pow_2) + mcopy(0x9d60, 0x7080, 0x80) + mstore(0x9de0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_2, r)) + mcopy(0x9e00, 0x7100, 0x80) + mstore(0x9e80, mulmod(mload(add(X1_POWERS_MPTR, 0x40)), x4_pow_2, r)) + mcopy(0x9ea0, 0x7180, 0x80) + mstore(0x9f20, mulmod(mload(add(X1_POWERS_MPTR, 0x60)), x4_pow_2, r)) + mcopy(0x9f40, 0x7200, 0x80) + mstore(0x9fc0, mulmod(mload(add(X1_POWERS_MPTR, 0x80)), x4_pow_2, r)) + mcopy(0x9fe0, 0x7280, 0x80) + mstore(0xa060, mulmod(mload(add(X1_POWERS_MPTR, 0xa0)), x4_pow_2, r)) + mcopy(0xa080, 0x7300, 0x80) + mstore(0xa100, mulmod(mload(add(X1_POWERS_MPTR, 0xc0)), x4_pow_2, r)) + mcopy(0xa120, 0x7380, 0x80) + mstore(0xa1a0, mulmod(mload(add(X1_POWERS_MPTR, 0xe0)), x4_pow_2, r)) + mcopy(0xa1c0, 0x7500, 0x80) + mstore(0xa240, x4_pow_3) + mcopy(0xa260, 0x7580, 0x80) + mstore(0xa2e0, mulmod(mload(add(X1_POWERS_MPTR, 0x20)), x4_pow_3, r)) + mcopy(0xa300, F_COM_MPTR, 0x80) + mstore(0xa380, x4_pow_4) if success { - success := staticcall(gas(), 0x0c, 0x6cc0, 0x2760, FINAL_COM_MPTR, 0x80) + // exact EIP-2537 G1MSM cost for 63 pair(s) + success := staticcall(436212, 0x0c, 0x7c40, 0x2760, FINAL_COM_MPTR, 0x80) success := and(success, eq(returndatasize(), 0x80)) } mstore(V_MPTR, v) @@ -2928,28 +3304,28 @@ contract Halo2Verifier { // Scale z*pi - vG before the final pairing check // pairing inputs (LHS = pi; RHS = final_com - v*G + x3*pi) mcopy(PAIRING_LHS_MPTR, PI_MPTR, 0x80) - mcopy(0x80, G1_BASE_MPTR, 0x80) - mstore(0x100, addmod(0, sub(r, mload(V_MPTR)), r)) + mcopy(0x1000, G1_BASE_MPTR, 0x80) + mstore(0x1080, addmod(0, sub(r, mload(V_MPTR)), r)) if success { - success := staticcall(gas(), 0x0c, 0x80, 0xa0, 0x80, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1000, 0xa0, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, FINAL_COM_MPTR, 0x80) + mcopy(0x1080, FINAL_COM_MPTR, 0x80) if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(0x100, PI_MPTR, 0x80) - mstore(0x180, mload(X3_MPTR)) + mcopy(0x1080, PI_MPTR, 0x80) + mstore(0x1100, mload(X3_MPTR)) if success { - success := staticcall(gas(), 0x0c, 0x100, 0xa0, 0x100, 0x80) + success := staticcall(G1MSM_GAS_1PAIR, 0x0c, 0x1080, 0xa0, 0x1080, 0x80) success := and(success, eq(returndatasize(), 0x80)) } if success { - success := staticcall(gas(), 0x0b, 0x80, 0x100, 0x80, 0x80) + success := staticcall(G1ADD_GAS, 0x0b, 0x1000, 0x100, 0x1000, 0x80) success := and(success, eq(returndatasize(), 0x80)) } - mcopy(PAIRING_RHS_MPTR, 0x80, 0x80) + mcopy(PAIRING_RHS_MPTR, 0x1000, 0x80) } } @@ -2980,13 +3356,19 @@ contract Halo2Verifier { // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) } diff --git a/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2VerifyingKey.sol b/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2VerifyingKey.sol index 0ee83d389..2091b9f1c 100644 --- a/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/target/sha-preimage-fixture-dump/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. @@ -116,19 +119,19 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x0660), 0x0000000000000000000000000000000000000000000000000000000000040011) // quotient_const mstore(add(payload, 0x0680), 0x0000000000000000000000000000000000000000000000000000001100000000) // quotient_const mstore(add(payload, 0x06a0), 0x0000000000000000000000000000000000000000000000000000000044000000) // quotient_const - mstore(add(payload, 0x06c0), 0x0000000000000000000000000000000000000000000000000000000000200000) // quotient_const - mstore(add(payload, 0x06e0), 0x0000000000000000000000000000000000000000000000000000000000000400) // quotient_const - mstore(add(payload, 0x0700), 0x0000000000000000000000000000000000000000000000000000000000400000) // quotient_const + mstore(add(payload, 0x06c0), 0x0000000000000000000000000000000000000000000000000000000000000400) // quotient_const + mstore(add(payload, 0x06e0), 0x0000000000000000000000000000000000000000000000000000000000200000) // quotient_const + mstore(add(payload, 0x0700), 0x0000000000000000000000000000000000000000000000000000000000000004) // quotient_const mstore(add(payload, 0x0720), 0x0000000000000000000000000000000000000000000000000000000000002000) // quotient_const - mstore(add(payload, 0x0740), 0x0000000000000000000000000000000000000000000000000000000000000004) // quotient_const - mstore(add(payload, 0x0760), 0x0000000000000000000000000000000000000000000000000000100000000000) // quotient_const + mstore(add(payload, 0x0740), 0x0000000000000000000000000000000000000000000000000000000000400000) // quotient_const + mstore(add(payload, 0x0760), 0x0000000000000000000000000000000000000000000000000000000000000010) // quotient_const mstore(add(payload, 0x0780), 0x0000000000000000000000000000000000000000000000000000000004000000) // quotient_const - mstore(add(payload, 0x07a0), 0x0000000000000000000000000000000000000000000000000000000000000010) // quotient_const - mstore(add(payload, 0x07c0), 0x0000000000000000000000000000000000000000000000000000000002000000) // quotient_const + mstore(add(payload, 0x07a0), 0x0000000000000000000000000000000000000000000000000000100000000000) // quotient_const + mstore(add(payload, 0x07c0), 0x0000000000000000000000000000000000000000000000000000000000000040) // quotient_const mstore(add(payload, 0x07e0), 0x0000000000000000000000000000000000000000000000000000000000000800) // quotient_const - mstore(add(payload, 0x0800), 0x0000000000000000000000000000000000000000000000000000000000000040) // quotient_const - mstore(add(payload, 0x0820), 0x0000000000000000000000000000000000000000000000000004000000000000) // quotient_const - mstore(add(payload, 0x0840), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x0800), 0x0000000000000000000000000000000000000000000000000000000002000000) // quotient_const + mstore(add(payload, 0x0820), 0x0000000000000000000000000000000000000000000000000000000000001000) // quotient_const + mstore(add(payload, 0x0840), 0x0000000000000000000000000000000000000000000000000004000000000000) // quotient_const mstore(add(payload, 0x0860), 0x0000000000000000000000000000000000000000000000000000000000040000) // quotient_const mstore(add(payload, 0x0880), 0x0000000000000000000000000000000000000000000000000000000000000080) // quotient_const mstore(add(payload, 0x08a0), 0x0000000000000000000000000000000000000000000000000000000000000008) // quotient_const @@ -136,19 +139,19 @@ contract Halo2VerifyingKey { mstore(add(payload, 0x08e0), 0x0000000000000000000000000000000000000000000000000000000000080000) // quotient_const mstore(add(payload, 0x0900), 0x0000000000000000000000000000000000000000000000000000000000020000) // quotient_const mstore(add(payload, 0x0920), 0x0000000000000000000000000000000000000000000000000000000100000000) // quotient_const - mstore(add(payload, 0x0940), 0x055860105b8005590008060d000b0200011b00001b00010558601058800558a0) // quotient_program - mstore(add(payload, 0x0960), 0x08060d000b0400011b00021b0003210001000007000158200258400358600458) // quotient_program - mstore(add(payload, 0x0980), 0x800558a00658c00758e00859000959800a59a00b59c00c59e00d5a000e5a200b) // quotient_program - mstore(add(payload, 0x09a0), 0x070000210001000007000158200258400f58601058801158a00658c00758e012) // quotient_program - mstore(add(payload, 0x09c0), 0x59001359801459a00b59c00c59e0155a00165a200b0800000917115a40135940) // quotient_program - mstore(add(payload, 0x09e0), 0x18105a60055a8008060d000b0900000919115a40135aa01a1359401b10596005) // quotient_program - mstore(add(payload, 0x0a00), 0x5a8008060d000b0a0000091c1159c01359e01d1358201e10584005586008060d) // quotient_program - mstore(add(payload, 0x0a20), 0x000b0a0001091f115a40135aa01a1359402013596021105a60055a8008060d00) // quotient_program - mstore(add(payload, 0x0a40), 0x0b0b000009221159c01359e01d13582019135840231058c005586008060d000b) // quotient_program - mstore(add(payload, 0x0a60), 0x0b00011c245920255940265960275a40185aa0285ac0295ae0105a60055a8008) // quotient_program - mstore(add(payload, 0x0a80), 0x060d000b0c0000090008105ac0115ac00d000b0c00010900081059201159200d) // quotient_program - mstore(add(payload, 0x0aa0), 0x000b0c0001090008105ae0115ae00d000b0c00011c0058800058a00059000059) // quotient_program - mstore(add(payload, 0x0ac0), 0x800059a0005a00005a20055a80135b002a08060d000b0d0000191f0000000000) // quotient_program + mstore(add(payload, 0x0940), 0x0567e0106b0005688008060d000b0200011b00001b00010567e0106800056820) // quotient_program + mstore(add(payload, 0x0960), 0x08060d000b0400011b00021b0003210001000007000167a00267c00367e00468) // quotient_program + mstore(add(payload, 0x0980), 0x000568200668400768600868800969000a69200b69400c69600d69800e69a00b) // quotient_program + mstore(add(payload, 0x09a0), 0x070000210001000007000167a00267c00f67e010680011682006684007686012) // quotient_program + mstore(add(payload, 0x09c0), 0x68801369001469200b69400c69601569801669a00b08000009181169c01368c0) // quotient_program + mstore(add(payload, 0x09e0), 0x171069e0056a0008060d000b090000091b1169c0136a201a1368c0191068e005) // quotient_program + mstore(add(payload, 0x0a00), 0x6a0008060d000b0a0000091e1169401369601d1367a01c1067c00567e008060d) // quotient_program + mstore(add(payload, 0x0a20), 0x000b0a000109211169c0136a201a1368c0201368e01f1069e0056a0008060d00) // quotient_program + mstore(add(payload, 0x0a40), 0x0b0b000009231169401369601d1367a01b1367c0221068400567e008060d000b) // quotient_program + mstore(add(payload, 0x0a60), 0x0b00011c2468a02568c02668e02769c0176a20286a40296a601069e0056a0008) // quotient_program + mstore(add(payload, 0x0a80), 0x060d000b0c0000090008106a40116a400d000b0c00010900081068a01168a00d) // quotient_program + mstore(add(payload, 0x0aa0), 0x000b0c0001090008106a60116a600d000b0c00011c0068000068200068800069) // quotient_program + mstore(add(payload, 0x0ac0), 0x000069200069800069a0056a00136a802a08060d000b0d0000191f0000000000) // quotient_program // Fixed-column commitment 0, stored as one // EIP-2537 padded uncompressed G1 slot. mstore(add(payload, 0x0ae0), 0x000000000000000000000000000000001197f0fef4c3a1846341b3c9bbaf1bab) // fixed_comms[0].x_hi diff --git a/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol b/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol index a7e0f52df..f5a45dead 100644 --- a/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol +++ b/proofs/solidity-verifier/templates/contracts/Halo2QuotientEvaluator.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Split Halo2 quotient numerator evaluator. /// @notice Reconstructs the scalar side of the linearization query for a generated verifier. diff --git a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol index c715ece09..a8ee8778f 100644 --- a/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol +++ b/proofs/solidity-verifier/templates/contracts/Halo2Verifier.sol @@ -1,5 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned, not floating. Two properties of this artifact are compiler- and +// optimiser-dependent, and neither is visible in the source: +// 1. The generated layout writes absolute addresses from TRANSCRIPT_MPTR +// upward. That is only safe while solc's stack-spill reservation stays +// below it -- measured 0x8c0 on 0.8.24 and 0x8e0 on 0.8.26+, so it is not +// a constant this file controls. verifyProof now asserts the separation. +// 2. Runtime size depends on --optimize-runs. Measured: 0.8.24 at runs=1 +// emits 29,567 bytes and 0.8.30 at runs=100000 emits 29,836 -- both over +// the EIP-170 24,576-byte limit, so neither can be deployed. Only the +// pinned (version, runs) pair is known to produce a deployable contract. +// A floating `^0.8.24` advertises compatibility this contract does not have. +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 KZG verifier. /// @notice Circuit-specialized verifier for Midfall/midnight-proofs Halo2 @@ -34,6 +45,43 @@ pragma solidity ^0.8.24; /// precompiles using identity inputs. Compile with Solidity >=0.8.24 and /// deploy only on chains/forks that support MCOPY and EIP-2537. contract Halo2Verifier { + // ---------------------------------------------------------------------- + // Typed failure taxonomy (P4/L-3, docs/audit/HALO2_VERIFIER_REVIEW). + // verifyProof is success-or-revert; these errors let integrators and + // incident responders distinguish malformed calldata from a swapped VK, + // a non-canonical scalar, a failed precompile, or a rejected proof. + // Constructor smoke probes intentionally keep bare reverts -- they report + // a chain-capability failure, and the deployment transaction identifies + // itself. The one constructor exception is the memory-layout guard + // (MF-2), which reports a BUILD fault and is typed on both paths. + // ---------------------------------------------------------------------- + /// @notice Calldata does not match the generated ABI shape (heads, + /// lengths, instance count, or exact calldatasize). + error BadCalldataShape(); + /// @notice The pinned verifying-key (or VK header cross-check) does not + /// match the generated constants. + error VkMismatch(); + /// @notice A public instance or proof scalar is >= the BLS12-381 scalar + /// modulus. + error NonCanonicalScalar(); + /// @notice A proof point violates the EIP-2537 padded encoding or its + /// coordinates are >= the base-field modulus. + error BadPointEncoding(); + /// @notice A precompile call failed or returned an unexpected size. + error PrecompileFailed(); + /// @notice The final pairing (or its staging) rejected the proof. + error ProofRejected(); + /// @notice The pinned quotient program or evaluator violated a structural + /// invariant (bad opcode, operand out of window, stack misuse, + /// or evaluator frame mismatch). + error QuotientProgramInvalid(); + /// @notice solc's stack-spill reservation overlaps the generated absolute + /// memory layout. This is a BUILD fault, not a proof fault: the + /// artifact was compiled with a (version, optimiser) pair whose + /// free-memory pointer starts at or above TRANSCRIPT_MPTR, so no + /// input can ever verify. Redeploy from the pinned toolchain. + error MemoryLayoutViolated(); + {% include "partials/verifier/Constants.sol" %} {% include "partials/verifier/PrecompileSmoke.sol" %} @@ -46,17 +94,33 @@ contract Halo2Verifier { /// bind the meaning of those instances separately: state roots, program /// identifiers, expected IVC outputs, chain/domain separation, and any /// protocol-specific authorization are outside this raw verifier ABI. + /// Wrapper obligations (replaceable verifier address, wrapper-held pause, + /// chainid/address/anti-replay binding) and the incident-response + /// playbook are REQUIREMENTS documented in + /// `docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md`. /// @dev Production renders are success-or-revert: accepted proofs return - /// `true`, while malformed calldata, invalid proof material, failed - /// precompiles, or mismatched pinned dependency code revert. Trace and gas - /// renders keep the same failure policy. + /// `true`; this function NEVER returns `false`. Every rejection reverts + /// with one of the typed errors declared above (BadCalldataShape, + /// VkMismatch, NonCanonicalScalar, BadPointEncoding, PrecompileFailed, + /// ProofRejected, QuotientProgramInvalid), so callers using + /// `if (!verifier.verifyProof(...))` never take the false branch — wrap + /// the call or decode the revert data instead. Trace and gas renders keep + /// the same failure policy. + /// @dev Calldata must be EXACTLY the ABI selector, proof bytes, and + /// generated instance words — `calldatasize` is pinned and any trailing + /// bytes revert with BadCalldataShape. In particular, ERC-2771 forwarders + /// and other calldata-appending relayers (multicall wrappers, paymaster + /// contexts) CANNOT call this contract directly; route such traffic + /// through an application wrapper that reassembles exact calldata. /// @dev The generated verifier uses absolute Yul memory addresses instead - /// of Solidity's free-memory pointer, but generated scratch starts at - /// `0x80` so Solidity's reserved memory prefix is preserved. The main + /// of Solidity's free-memory pointer. Generated scratch starts at + /// `TRANSCRIPT_MPTR`, which leaves Solidity's reserved prefix *and* solc's + /// stack-spill reservation below it untouched; the assembly block asserts + /// that separation on entry rather than assuming it. The main /// assembly block remains terminal: accepted proofs return from assembly /// and all rejected inputs revert. Do not inline this body into Solidity /// code that continues executing after verification without reviewing the - /// memory strategy; see `docs/MEMORY_LAYOUT.md`. + /// memory strategy; see `docs/architecture/MEMORY_LAYOUT.md`. /// @param proof Solidity-facing proof bytes, with G1 elements repacked into EIP-2537 padded uncompressed form. /// @param instances Public instance scalars encoded as canonical BLS12-381 scalar-field words. /// @return Always `true` for accepted proofs; invalid proofs revert instead of returning `false`. @@ -73,7 +137,10 @@ contract Halo2Verifier { // valid Midfall proof stream. assembly ("memory-safe") { if iszero(and(eq(calldataload({{ abi_selector_bytes|hex() }}), {{ abi_proof_head_offset|hex() }}), eq(calldataload({{ abi_instances_head_cptr|hex() }}), sub(NUM_INSTANCE_CPTR, {{ abi_selector_bytes|hex() }})))) { - revert(0, 0) + // BadCalldataShape() -- fail() is not in scope in this early + // guard block, so write the selector inline. + mstore(0x00, shl(224, ERR_BAD_CALLDATA_SHAPE)) + revert(0x00, 0x04) } } @@ -93,10 +160,41 @@ contract Halo2Verifier { {%- when None %} {%- endmatch %} assembly ("memory-safe") { + // The `memory-safe` annotation above is what enables solc's + // stack-to-memory mover, which reserves spill slots upward from + // 0x80. The generated layout below writes absolute addresses from + // TRANSCRIPT_MPTR upward and never consults the free-memory + // pointer, so the two regions must not meet. The size of that + // reservation is compiler-version and optimiser dependent, so + // assert the invariant in the deployed bytecode instead of relying + // on a generator-side test the integrator never runs. ~6 gas. + // + // MF-12: by the letter of Solidity's memory-safety contract this + // annotation is a lie -- the block writes memory it never + // allocated through the free-memory pointer. Three properties + // make it safe HERE, and all three must hold together: this + // block is terminal (no Solidity executes after it), the pragma + // is pinned so codegen cannot shift underneath it, and the guard + // below fails closed if the spill reservation ever reaches the + // generated layout. Lifting this body into a non-terminal + // context, or unpinning the pragma, invalidates the annotation. + // + // MF-2: this is the only on-chain guard against a recompile that + // silently moves the spill region, and the failure it catches is + // permanent (no input can verify). `fail()` is not in scope this + // early, so write the MemoryLayoutViolated() selector inline + // rather than reverting bare -- an empty revert here is + // indistinguishable from every other empty revert, which is + // exactly the wrong signal for a build fault. + if gt(mload(0x40), TRANSCRIPT_MPTR) { + mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED)) + revert(0x00, 0x04) + } + // This block owns the call-frame memory and remains terminal. - // Generated scratch starts at TRANSCRIPT_MPTR (0x80), preserving + // Generated scratch starts at TRANSCRIPT_MPTR, preserving // Solidity's reserved scratch, free-memory-pointer, and zero-slot - // words. See docs/MEMORY_LAYOUT.md. + // words. See docs/architecture/MEMORY_LAYOUT.md. // =============================================================== // Helpers: modexp, transcript, EIP-2537 calls // =============================================================== diff --git a/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol b/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol index 955ae1072..756556d4d 100644 --- a/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol +++ b/proofs/solidity-verifier/templates/contracts/Halo2VerifyingKey.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.24; +// Pinned to match the verifier, so both halves of a deployment are provably +// built by one toolchain. (This contract's runtime is pure returned data, so +// its codehash is compiler-independent -- the pin is for the pair, not for it.) +pragma solidity 0.8.30; /// @title Halo2 BLS12-381 verifying-key payload. /// @notice Contract whose deployed runtime is `INVALID || generated verifier-key payload`. diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul index a9ec4d9e7..41c625e58 100644 --- a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul +++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientHelpers.yul @@ -1,3 +1,13 @@ + // Revert with the QuotientProgramInvalid() selector + // (bytes4(keccak256) = 0x3cc81b89; pinned by + // p4_error_selectors_match_declared_errors). Defined here rather + // than in AssemblyHelpers.yul because the quotient VM renders in + // BOTH the main verifier and the standalone evaluator assembly. + function q_program_fail() { + mstore(0x00, shl(224, 0x3cc81b89)) + revert(0x00, 0x04) + } + // Optional quotient helper functions. Each one is rendered only // when the Rust lowering pass recognized the corresponding // expression shape in this generated verifier. They are pure Fr diff --git a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul index e206aed4e..005228fb0 100644 --- a/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul +++ b/proofs/solidity-verifier/templates/partials/quotient_numerator/QuotientNumeratorBlock.yul @@ -1,3 +1,60 @@ +{#- + P12 (L-6, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md) runtime operand + clamps. The VM program is trusted only through the VK codehash pin; + build-time validate_quotient_mem_ptrs proves exact window membership, but + the deployed interpreter re-checks every decoded memory-pointer operand + against the coarse [operand_lo, operand_hi] union of the planned read + windows so a substituted or corrupted VK payload cannot read arbitrary + verifier state. Single-comparison form: unsigned wrap makes + sub(ptr, lo) > (hi - lo) cover both bounds (~6 gas per operand). + u8 constant-table indexes stay unguarded: their drift is bounded to + 0x1FE0 bytes inside the VK-reserved region and covered by build-time + validate_quotient_const_slots. MF-3: the u16 forms do NOT share that + argument -- an out-of-range u16 index reaches 0x1FFFE0 bytes past the + table, well outside the pinned payload -- so those three sites are + clamped against the rendered table length. +-#} +{%- macro q_ptr_guard(ptr) %} + if gt(sub({{ ptr }}, {{ program.operand_lo|hex() }}), {{ (program.operand_hi - program.operand_lo)|hex() }}) { q_program_fail() } +{%- endmacro %} +{#- 7-word limb-vector bases must leave room for the whole vector. -#} +{%- macro q_vec7_guard(ptr) %} + if gt(sub({{ ptr }}, {{ program.operand_lo|hex() }}), {{ (program.operand_hi - program.operand_lo - 0xc0)|hex() }}) { q_program_fail() } +{%- endmacro %} +{#- Pops revert at the stack floor; the terminal eq(q_sp, base) check + cannot catch a BALANCED underflow, this can. -#} +{%- macro q_pop_guard() %} + if eq(q_sp, {{ program.stack_mptr|hex() }}) { q_program_fail() } +{%- endmacro %} +{#- + MF-3 interpreter fail-closed guards. The program is trusted through the VK + codehash pin, so none of these are reachable on-chain with a well-formed + artifact; they are containment for a future GENERATOR bug, in the same + spirit as the P12 operand clamps above. Each closes a hole the terminal + end-of-program checks provably cannot see: + + - q_top_guard: FOLD_MAIN/FOLD_SELECTOR consume the cached top. With + q_has_top clear, the fold silently re-folds a STALE q_top and both + terminal checks (q_has_top == 0, q_sp == base) still pass. + - q_stack_empty_guard: native callbacks used to RESET q_sp to the base + rather than assert it, so operands spilled by a preceding partial + expression were discarded with no trace -- an identity would drop out + of nu_y(x) while the program still ended balanced. + - q_const_guard: constant-table indexes are decoded from program bytes + and were unclamped, so an out-of-range index reads whatever follows the + table (program bytes, commitments) as an Fr constant. + - the inlined spill-ceiling check at every push site: q_sp walks + upward with no ceiling of its own. +-#} +{%- macro q_top_guard() %} + if iszero(q_has_top) { q_program_fail() } +{%- endmacro %} +{%- macro q_stack_empty_guard() %} + if iszero(eq(q_sp, {{ program.stack_mptr|hex() }})) { q_program_fail() } +{%- endmacro %} +{%- macro q_const_guard(idx) %} + if iszero(lt({{ idx }}, {{ program.num_consts }})) { q_program_fail() } +{%- endmacro %} // =============================================================== // Batched identity numerator / linearization target. // @@ -126,7 +183,14 @@ { // q_y_power holds y^i at the current loop index. let q_y_power := 1 - // Start at i=1 because y^0 = 1 is implicit and never read. + // Slot 0 holds y^0 = 1. Codegen never emits a read of it + // (FOLD_SELECTOR guards on a nonzero gap, and + // selector_tail_updates drops zero tails), but the tail + // block multiplies by mload(selector_power_mptr + offset) + // unconditionally -- so initialize the slot rather than + // leaving correctness to two filters in another file. + mstore({{ program.selector_power_mptr|hex() }}, 1) + // Start at i=1 because y^0 = 1 is written above. for { let q_y_power_i := 1 } lt(q_y_power_i, {{ program.selector_max_power + 1 }}) { q_y_power_i := add(q_y_power_i, 1) } { // Advance from y^(i-1) to y^i modulo Fr. q_y_power := mulmod(q_y_power, y, r) @@ -169,18 +233,103 @@ // q_has_top = 0 means the VM stack is empty. let q_has_top := 0 - // q_program opcode summary: - // 0x01/0x09 push const 0x02/0x05 push memory - // 0x03/0x04 push token ptr 0x06 add, 0x07 mul, 0x08 neg - // 0x0a fold main identity 0x0b fold selector identity - // 0x0c..0x11 add/mul const or memory into top - // 0x12..0x16 fused add-mul runs - // 0x17/0x18 reserved - // 0x19 native permutation 0x1b native heavy identity - // 0x1c LIN7 0x1d BILIN7_ROW - // 0x1e BILIN7_PAIRWISE 0x1f native lookup - // 0x20 POW5 0x21 MODARITH7 - // 0x22 AFFINE_SUM + // q_program opcode summary. Rendered from the same + // program.op_usage predicates that gate the interpreter's + // case arms below, so this artifact documents exactly the + // opcodes its program can contain -- no more, no fewer. + {%- if program.op_usage.push_const %} + // {{ template_constants.quotient_vm.op.push_const|hex() }} push_const + {%- endif %} + {%- if program.op_usage.push_mem_literal %} + // {{ template_constants.quotient_vm.op.push_mem_literal|hex() }} push_mem_literal + {%- endif %} + {%- if program.op_usage.push_mem_token %} + // {{ template_constants.quotient_vm.op.push_mem_token|hex() }} push_mem_token + {%- endif %} + {%- if program.op_usage.push_mem_token_offset %} + // {{ template_constants.quotient_vm.op.push_mem_token_offset|hex() }} push_mem_token_offset + {%- endif %} + {%- if program.op_usage.push_mem_u16 %} + // {{ template_constants.quotient_vm.op.push_mem_u16|hex() }} push_mem_u16 + {%- endif %} + {%- if program.op_usage.add %} + // {{ template_constants.quotient_vm.op.add|hex() }} add + {%- endif %} + {%- if program.op_usage.mul %} + // {{ template_constants.quotient_vm.op.mul|hex() }} mul + {%- endif %} + {%- if program.op_usage.neg %} + // {{ template_constants.quotient_vm.op.neg|hex() }} neg + {%- endif %} + {%- if program.op_usage.push_const_u8 %} + // {{ template_constants.quotient_vm.op.push_const_u8|hex() }} push_const_u8 + {%- endif %} + {%- if program.op_usage.fold_main %} + // {{ template_constants.quotient_vm.op.fold_main|hex() }} fold_main + {%- endif %} + {%- if program.op_usage.fold_selector %} + // {{ template_constants.quotient_vm.op.fold_selector|hex() }} fold_selector + {%- endif %} + {%- if program.op_usage.add_const_u8 %} + // {{ template_constants.quotient_vm.op.add_const_u8|hex() }} add_const_u8 + {%- endif %} + {%- if program.op_usage.mul_const_u8 %} + // {{ template_constants.quotient_vm.op.mul_const_u8|hex() }} mul_const_u8 + {%- endif %} + {%- if program.op_usage.add_const %} + // {{ template_constants.quotient_vm.op.add_const|hex() }} add_const + {%- endif %} + {%- if program.op_usage.mul_const %} + // {{ template_constants.quotient_vm.op.mul_const|hex() }} mul_const + {%- endif %} + {%- if program.op_usage.add_mem_u16 %} + // {{ template_constants.quotient_vm.op.add_mem_u16|hex() }} add_mem_u16 + {%- endif %} + {%- if program.op_usage.mul_mem_u16 %} + // {{ template_constants.quotient_vm.op.mul_mem_u16|hex() }} mul_mem_u16 + {%- endif %} + {%- if program.op_usage.add_mul_mem_mem_const_u8 %} + // {{ template_constants.quotient_vm.op.add_mul_mem_mem_const_u8|hex() }} add_mul_mem_mem_const_u8 + {%- endif %} + {%- if program.op_usage.add_mul_const_u8_mem_u16 %} + // {{ template_constants.quotient_vm.op.add_mul_const_u8_mem_u16|hex() }} add_mul_const_u8_mem_u16 + {%- endif %} + {%- if program.op_usage.add_mul_mem_mem %} + // {{ template_constants.quotient_vm.op.add_mul_mem_mem|hex() }} add_mul_mem_mem + {%- endif %} + {%- if program.op_usage.run_add_mul_mem_mem_const_u8 %} + // {{ template_constants.quotient_vm.op.run_add_mul_mem_mem_const_u8|hex() }} run_add_mul_mem_mem_const_u8 + {%- endif %} + {%- if program.op_usage.run_add_mul_const_u8_mem_u16 %} + // {{ template_constants.quotient_vm.op.run_add_mul_const_u8_mem_u16|hex() }} run_add_mul_const_u8_mem_u16 + {%- endif %} + {%- if program.op_usage.affine_sum %} + // {{ template_constants.quotient_vm.op.affine_sum|hex() }} affine_sum + {%- endif %} + {%- if program.op_usage.native_permutation %} + // {{ template_constants.quotient_vm.op.native_permutation|hex() }} native_permutation + {%- endif %} + {%- if program.op_usage.native_lookup %} + // {{ template_constants.quotient_vm.op.native_lookup|hex() }} native_lookup + {%- endif %} + {%- if program.op_usage.native_identity %} + // {{ template_constants.quotient_vm.op.native_identity|hex() }} native_identity + {%- endif %} + {%- if program.op_usage.lin7 %} + // {{ template_constants.quotient_vm.op.lin7|hex() }} lin7 + {%- endif %} + {%- if program.op_usage.bilin7_row %} + // {{ template_constants.quotient_vm.op.bilin7_row|hex() }} bilin7_row + {%- endif %} + {%- if program.op_usage.bilin7_pairwise %} + // {{ template_constants.quotient_vm.op.bilin7_pairwise|hex() }} bilin7_pairwise + {%- endif %} + {%- if program.op_usage.modarith7 %} + // {{ template_constants.quotient_vm.op.modarith7|hex() }} modarith7 + {%- endif %} + {%- if program.op_usage.pow5 %} + // {{ template_constants.quotient_vm.op.pow5|hex() }} pow5 + {%- endif %} // // The default IVC verifier uses one physical encoding for the // logical VM: compact byte-oriented opcodes with variable-width @@ -191,7 +340,7 @@ This comment documents the interpreter source without being emitted into generated Solidity. Runtime behavior lives in the switch blocks below; opcode numbers and operand layouts are - defined in src/codegen/quotient/mod.rs. + defined in src/lowering/quotient_numerator/vm/mod.rs. Shared stack model: - q_top caches the top stack value. @@ -356,9 +505,11 @@ // Fr words, so shl(5, const_idx) converts an index to // a byte offset. let qconst := shr(240, mload(q_pc)) +{%- call q_const_guard("qconst") %} // Push semantics: spill the old cached top, if any, // then install the loaded constant as the new q_top. if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -375,9 +526,11 @@ // the smaller u16 form or token map. let q_ptr := shr(224, mload(q_pc)) q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + {%- call q_ptr_guard("q_ptr") %} // Load one canonical Fr word from generated memory and // push it through the cached-top stack discipline. if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -424,8 +577,9 @@ {%- endif %} // A token not advertised by the VM usage manifest is // impossible for valid generated bytecode. - default { revert(0, 0) } + default { q_program_fail() } if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -474,8 +628,10 @@ {%- if program.mem_usage.instance_eval %} case {{ template_constants.quotient_vm.mem.instance_eval|hex() }} { q_ptr := add(INSTANCE_EVAL_MPTR, q_off) } {%- endif %} - default { revert(0, 0) } + default { q_program_fail() } + {%- call q_ptr_guard("q_ptr") %} if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -491,7 +647,9 @@ // 64 KiB when this compact form is emitted. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_ptr") %} if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -504,6 +662,7 @@ case {{ template_constants.quotient_vm.op.add|hex() }} { // The safety validator guarantees a spilled operand // exists before ADD. q_top is the right operand. + {%- call q_pop_guard() %} q_sp := sub(q_sp, 0x20) q_top := addmod(mload(q_sp), q_top, r) } @@ -513,6 +672,7 @@ case {{ template_constants.quotient_vm.op.mul|hex() }} { // Same stack contract as ADD, with multiplication // reduced directly modulo Fr. + {%- call q_pop_guard() %} q_sp := sub(q_sp, 0x20) q_top := mulmod(mload(q_sp), q_top, r) } @@ -540,6 +700,7 @@ // constant table has fewer than 256 referenced slots. let qconst := byte(0, mload(q_pc)) if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -575,7 +736,10 @@ // constant tables. let qconst := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) - q_top := addmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) +{%- call q_const_guard("qconst") %} + let q_const_ptr := add(q_const_mptr, shl(5, qconst)) + {%- call q_ptr_guard("q_const_ptr") %} + q_top := addmod(q_top, mload(q_const_ptr), r) } {%- endif %} {%- if program.op_usage.mul_const %} @@ -584,7 +748,10 @@ // Two-byte constant-index multiply. let qconst := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) - q_top := mulmod(q_top, mload(add(q_const_mptr, shl(5, qconst))), r) +{%- call q_const_guard("qconst") %} + let q_const_ptr := add(q_const_mptr, shl(5, qconst)) + {%- call q_ptr_guard("q_const_ptr") %} + q_top := mulmod(q_top, mload(q_const_ptr), r) } {%- endif %} {%- if program.op_usage.add_mem_u16 %} @@ -594,6 +761,7 @@ // already range-checked Fr scalar in verifier memory. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_ptr") %} q_top := addmod(q_top, mload(q_ptr), r) } {%- endif %} @@ -603,6 +771,7 @@ // In-place multiply by a planned memory word. let q_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_ptr") %} q_top := mulmod(q_top, mload(q_ptr), r) } {%- endif %} @@ -617,6 +786,8 @@ let q_rhs := and(shr(224, q_word), 0xffff) let qconst := byte(4, q_word) q_pc := add(q_pc, 5) + {%- call q_ptr_guard("q_lhs") %} + {%- call q_ptr_guard("q_rhs") %} q_top := addmod( q_top, mulmod( @@ -636,6 +807,7 @@ let q_ptr := shr(240, q_word) let qconst := byte(2, q_word) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_top := addmod( q_top, mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), @@ -652,6 +824,8 @@ let q_lhs := shr(240, q_word) let q_rhs := and(shr(224, q_word), 0xffff) q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + {%- call q_ptr_guard("q_lhs") %} + {%- call q_ptr_guard("q_rhs") %} q_top := addmod(q_top, mulmod(mload(q_lhs), mload(q_rhs), r), r) } {%- endif %} @@ -670,6 +844,8 @@ let q_rhs := and(shr(224, q_word), 0xffff) let qconst := byte(4, q_word) q_pc := add(q_pc, 5) + {%- call q_ptr_guard("q_lhs") %} + {%- call q_ptr_guard("q_rhs") %} q_top := addmod( q_top, mulmod( @@ -695,6 +871,7 @@ let q_ptr := shr(240, q_word) let qconst := byte(2, q_word) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_top := addmod( q_top, mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), @@ -723,6 +900,7 @@ let q_ptr := shr(240, q_word) let qconst := byte(2, q_word) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_top := addmod( q_top, mulmod(mload(q_ptr), mload(add(q_const_mptr, shl(5, qconst))), r), @@ -737,6 +915,8 @@ let q_rhs := and(shr(224, q_word), 0xffff) let qconst := byte(4, q_word) q_pc := add(q_pc, 5) + {%- call q_ptr_guard("q_lhs") %} + {%- call q_ptr_guard("q_rhs") %} q_top := addmod( q_top, mulmod( @@ -779,6 +959,7 @@ // u16 ptr} pairs. The result is pushed as a fresh // stack value. if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -790,6 +971,7 @@ let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -813,8 +995,10 @@ // loaded once and reused for all seven products. let q_lhs := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_lhs") %} let q_lhs_value := mload(q_lhs) if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -824,6 +1008,7 @@ let qconst := byte(0, q_word) let q_rhs := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_rhs") %} q_acc := addmod( q_acc, mulmod( @@ -856,9 +1041,12 @@ let q_lhs_base := shr(240, q_word) let q_rhs_base := and(shr(224, q_word), 0xffff) q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + {%- call q_vec7_guard("q_lhs_base") %} + {%- call q_vec7_guard("q_rhs_base") %} let q_coeff_pc := q_pc q_pc := add(q_pc, {{ template_constants.quotient_vm.limb_pairwise_coeffs }}) if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -913,6 +1101,7 @@ // whole identity is gated by mload(q_cond_ptr). q_cond_ptr := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_cond_ptr") %} } let q_acc := 0 @@ -936,6 +1125,7 @@ q_pc := add(q_pc, 5) if q_has_top { + if iszero(lt(q_sp, {{ program.stack_hi|hex() }})) { q_program_fail() } mstore(q_sp, q_top) q_sp := add(q_sp, 0x20) } @@ -947,6 +1137,7 @@ let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -959,12 +1150,14 @@ for { let q_row_block := 0 } lt(q_row_block, q_row_count) { q_row_block := add(q_row_block, 1) } { let q_lhs := shr(240, mload(q_pc)) q_pc := add(q_pc, 2) + {%- call q_ptr_guard("q_lhs") %} let q_lhs_value := mload(q_lhs) for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { let q_word := mload(q_pc) let qconst := byte(0, q_word) let q_rhs := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_rhs") %} q_acc := addmod( q_acc, mulmod( @@ -984,6 +1177,8 @@ let q_lhs_base := shr(240, q_pair_word) let q_rhs_base := and(shr(224, q_pair_word), 0xffff) q_pc := add(q_pc, {{ template_constants.quotient_vm.byte_u32_bytes|hex() }}) + {%- call q_vec7_guard("q_lhs_base") %} + {%- call q_vec7_guard("q_rhs_base") %} let q_coeff_pc := q_pc q_pc := add(q_pc, {{ template_constants.quotient_vm.limb_pairwise_coeffs }}) for { let q_i := 0 } lt(q_i, {{ template_constants.quotient_vm.limb_count }}) { q_i := add(q_i, 1) } { @@ -1009,6 +1204,7 @@ let qconst := byte(0, q_word) let q_ptr := and(shr(232, q_word), 0xffff) q_pc := add(q_pc, 3) + {%- call q_ptr_guard("q_ptr") %} q_acc := addmod( q_acc, mulmod(mload(add(q_const_mptr, shl(5, qconst))), mload(q_ptr), r), @@ -1023,6 +1219,8 @@ let q_lhs := and(shr(232, q_word), 0xffff) let q_rhs := and(shr(216, q_word), 0xffff) q_pc := add(q_pc, 5) + {%- call q_ptr_guard("q_lhs") %} + {%- call q_ptr_guard("q_rhs") %} q_acc := addmod( q_acc, mulmod( @@ -1061,7 +1259,7 @@ // stack. The Rust memory planner must reserve enough // words for structured_permutation_scratch_words(meta) // whenever this opcode can appear. - q_sp := {{ program.stack_mptr|hex() }} +{%- call q_stack_empty_guard() %} // The generated lines below call the same fold snippets // used by interpreted expressions, so trace IDs and // y-batch positions remain contiguous. @@ -1087,7 +1285,7 @@ // f+beta/prefix/suffix scratch rather than as a // conventional VM stack. The Rust memory planner must // reserve structured_lookup_scratch_words(meta). - q_sp := {{ program.stack_mptr|hex() }} +{%- call q_stack_empty_guard() %} // Generated LogUp code follows the same y-batch order // as the Rust identity stream. {%- for line in quotient_native_lookup_computation %} @@ -1112,7 +1310,7 @@ // interpreter stack before dispatching. q_top := 0 q_has_top := 0 - q_sp := {{ program.stack_mptr|hex() }} +{%- call q_stack_empty_guard() %} // Native identity sub-cases are generated from selected heavy gate identities. switch q_native_idx {%- for code_block in quotient_native_identity_computations %} @@ -1122,7 +1320,7 @@ {%- endfor %} } {%- endfor %} - default { revert(0, 0) } + default { q_program_fail() } } {%- endif %} {%- if program.op_usage.fold_main %} @@ -1130,6 +1328,7 @@ case {{ template_constants.quotient_vm.op.fold_main|hex() }} { // q_top is the complete value of one fully evaluated // identity at x. It leaves the expression stack here. +{%- call q_top_guard() %} let q_eval := q_top q_has_top := 0 {%- if self.trace %} @@ -1154,6 +1353,12 @@ q_pc := add(q_pc, 3) let q_sel_idx := shr(16, q_selector_payload) let q_sel_gap := and(q_selector_payload, 0xffff) + // P12: the bucket index addresses the SELECTOR_ACC + // region and the gap indexes the y-power table; both + // are codegen-known sizes, so clamp before the writes. + if iszero(lt(q_sel_idx, {{ program.num_selector_buckets }})) { q_program_fail() } + if gt(q_sel_gap, {{ program.selector_max_power|hex() }}) { q_program_fail() } +{%- call q_top_guard() %} let q_eval := q_top q_has_top := 0 {%- if self.trace %} @@ -1181,15 +1386,21 @@ {%- endif %} // Invalid generated bytecode should fail closed. 0x1a intentionally lands here. default { - revert(0, 0) + q_program_fail() } } // The VK-pinned bytecode must end exactly at q_end and every // identity must have been consumed by a fold/native callback. // This catches malformed generator output whose final opcode // over-reads operands or leaves a partial expression live. - if iszero(eq(q_pc, q_end)) { revert(0, 0) } - if q_has_top { revert(0, 0) } + if iszero(eq(q_pc, q_end)) { q_program_fail() } + if q_has_top { q_program_fail() } + // The spilled stack must also be balanced. A FOLD executed + // with more than one operand live consumes only the cached + // top, leaving abandoned words below q_sp with q_has_top + // clear -- so both checks above pass while an operand of the + // identity has been silently dropped from nu_y(x). + if iszero(eq(q_sp, {{ program.stack_mptr|hex() }})) { q_program_fail() } // Structured post-VM suffix. The current default uses this for // regular trash constraints: it is smaller than fully unrolled diff --git a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul index 3a5d5eb77..41e2b46a0 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/AccumulatorHelpers.yul @@ -25,7 +25,13 @@ // public input. `first_adjust` removes the identity flag // base from the first x word when present. let packed := calldataload(add(src, mul(div(i, limbs_per_word), 0x20))) - if and(iszero(div(i, limbs_per_word)), first_adjust) { + // `and` here is bitwise, so it must not be fed the raw + // `first_adjust` (a radix base, i.e. a high power of two): + // `iszero(...)` is 0 or 1 and shares no bit with it, which + // would make the guard false for every call. Subtracting is + // already a no-op when `first_adjust` is zero, so gate on + // the word index alone. + if iszero(div(i, limbs_per_word)) { packed := sub(packed, first_adjust) } // Select limb i from its packed field word. The mod/div @@ -193,6 +199,14 @@ // If x carried the identity flag, both decoded // coordinates must be zero after shifting. Any other y // value would be a malformed infinity encoding. + // + // Unreachable by construction (audit I-2/I-3): the + // whole-point sentinel check above already accepted + // every encoding in which x carries the identity flag + // -- the packed codec is a bijection, so an x flagged + // as identity with a sentinel mismatch cannot decode + // here. Kept as defence in depth for future codec + // changes rather than as a live branch. ok := and(ok, iszero(or(or(x_hi, x_lo), or(y_hi, y_lo)))) mstore(dst, 0) mstore(add(dst, 0x20), 0) @@ -230,7 +244,17 @@ // 3. folds the RHS carried point and fixed-base scalar tail into // ACC_RHS_MPTR, leaving ACC_LHS_MPTR / ACC_RHS_MPTR ready for // randomized batching in FinalPairing.yul. - function validate_public_accumulator(success, r) -> out { + // `r` is consumed only by the canonicality guards in the + // carried-scalar and fixed-base-tail arms; renders whose + // accumulator layout has neither (e.g. point_pair with no tail) + // legally leave it unused. + // MF-4: `precompile_failed` separates a G1MSM staticcall that + // could not run (chain/gas fault) from a public-input point this + // verifier decoded and rejected (bad packing, out-of-field + // coordinate, non-canonical identity encoding, or a point the + // precompile found off-curve/out-of-subgroup). Both fail closed at + // the call site; only the second is a BadPointEncoding. + function validate_public_accumulator(success, r) -> out, precompile_failed { out := success let bits := {{ self.expected_num_acc_limb_bits }} let n := {{ self.expected_num_acc_limbs }} @@ -259,6 +283,11 @@ // Carried-scalar layout: the circuit exposes the scalar // that multiplies the carried LHS point. let lhs_scalar := calldataload(lhs_scalar_ptr) + // Canonicality is enforced here rather than relying on the + // later instance-absorption loop: G1MSM reduces scalars + // mod r implicitly, so s and s+r would be indistinguishable + // inside this helper. + out := and(out, lt(lhs_scalar, r)) {%- else %} // Already-collapsed point-pair layout: carried scalars are // implicit one. @@ -278,8 +307,9 @@ // Single-pair MSM output overwrites ACC_LHS_MPTR with // lhs_scalar * decoded_lhs. If lhs_scalar is one, this // is also a curve/subgroup validation round-trip. - out := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + out := staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, {{ template_constants.g1_msm_pair_bytes|hex() }}, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + precompile_failed := iszero(out) } } @@ -319,6 +349,7 @@ {%- if self.expected_acc_has_carried_scalars %} // Explicit carried RHS scalar. let rhs_scalar := calldataload(rhs_scalar_ptr) + out := and(out, lt(rhs_scalar, r)) {%- else %} // Implicit unit scalar for already-collapsed point pairs. let rhs_scalar := 1 @@ -347,6 +378,9 @@ // corresponding base point is embedded in verifier memory at // {{ base_mptr|hex() }}. let fixed_scalar_{{ loop.index0 }} := calldataload(fixed_scalar_ptr) + // Reject non-canonical tail scalars before the negation below: + // for s >= r, `mod(sub(r, s), r)` is not -s mod r. + out := and(out, lt(fixed_scalar_{{ loop.index0 }}, r)) {%- if negate_scalar %} // Some accumulator bases are represented with a negated scalar // so the MSM can reuse the generated positive base point. @@ -379,8 +413,11 @@ // // The precompile also validates every nonzero fixed // base embedded by codegen and the carried RHS point. + // ACC_RHS_MSM_GAS is the compile-time worst case + // (every tail scalar nonzero); acc_msm_len can only + // select a same-size-or-smaller MSM at runtime. out := staticcall( - gas(), + ACC_RHS_MSM_GAS, {{ template_constants.eip2537.g1msm_address|hex() }}, acc_scratch, acc_msm_len, @@ -388,6 +425,7 @@ {{ template_constants.g1_bytes|hex() }} ) out := and(out, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) + precompile_failed := iszero(out) } } // The caller checks `out` and reverts before transcript work if diff --git a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul index f3ddbfc73..604d642d5 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/AssemblyHelpers.yul @@ -1,3 +1,11 @@ + // Revert with a 4-byte custom-error selector (P4/L-3). Writing at + // 0x00 is Solidity's legal scratch space and never touches the + // generated layout, which starts at TRANSCRIPT_MPTR. + function fail(sel) { + mstore(0x00, shl(224, sel)) + revert(0x00, 0x04) + } + // Inverse of a Fr scalar via modexp(x, r-2, r). The verifier // calls this only after transcript absorption is complete, so it // reuses the dead transcript buffer just below VK_MPTR instead of @@ -5,8 +13,14 @@ // when the VK payload becomes smaller. function scalar_inv(x) -> inv { // Zero has no multiplicative inverse in Fr; callers rely on a - // revert here rather than a bogus modexp result. - if iszero(x) { revert(0, 0) } + // revert here rather than a bogus modexp result. Check the + // full canonical range, not just the literal word 0: for any + // x congruent to 0 mod r (x = r, say) modexp returns 0, which + // downstream mulmod chains would silently absorb. Every + // current call site feeds addmod/mulmod output, so this only + // guards against a future emitter passing a raw scalar. + if iszero(lt(x, FR_MODULUS)) { fail(ERR_NON_CANONICAL_SCALAR) } + if iszero(x) { fail(ERR_NON_CANONICAL_SCALAR) } let p := {{ memory.scalar_inv_scratch_mptr|hex() }} // EIP-198 modexp frame: // [base_len, exp_len, mod_len, base, exponent, modulus] @@ -16,8 +30,8 @@ mstore(add(p, {{ template_constants.modexp.base_offset|hex() }}), x) mstore(add(p, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2)) mstore(add(p, {{ template_constants.modexp.mod_offset|hex() }}), FR_MODULUS) - if iszero(staticcall(gas(), {{ template_constants.modexp.address|hex() }}, p, {{ template_constants.modexp.frame_bytes|hex() }}, p, {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } - if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, p, {{ template_constants.modexp.frame_bytes|hex() }}, p, {{ template_constants.modexp.output_bytes|hex() }})) { fail(ERR_PRECOMPILE_FAILED) } + if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { fail(ERR_PRECOMPILE_FAILED) } inv := mload(p) } @@ -77,16 +91,16 @@ let x_lo := calldataload(add(cptr, 0x20)) let y_hi_word := calldataload(add(cptr, 0x40)) let y_lo := calldataload(add(cptr, 0x60)) - if shr(128, x_hi_word) { revert(0, 0) } - if shr(128, y_hi_word) { revert(0, 0) } + if shr(128, x_hi_word) { fail(ERR_BAD_POINT_ENCODING) } + if shr(128, y_hi_word) { fail(ERR_BAD_POINT_ENCODING) } let x_hi := and(x_hi_word, 0xffffffffffffffffffffffffffffffff) let y_hi := and(y_hi_word, 0xffffffffffffffffffffffffffffffff) if iszero(or(lt(x_hi, BLS_P_HI), and(eq(x_hi, BLS_P_HI), iszero(gt(x_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } if iszero(or(lt(y_hi, BLS_P_HI), and(eq(y_hi, BLS_P_HI), iszero(gt(y_lo, BLS_P_MINUS_ONE_LO))))) { - revert(0, 0) + fail(ERR_BAD_POINT_ENCODING) } // Memcpy the 4 calldata words (128 bytes) verbatim @@ -124,7 +138,16 @@ // The function returns a boolean instead of reverting so callers // can combine it with other `success` plumbing until a section // boundary decides whether to fail closed. - function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret { + // + // MF-4: the second return value separates a FAILED PRECOMPILE + // (staticcall reverted / OOG'd / returned the wrong size -- a + // chain or gas-schedule fault) from a REJECTED INPUT (a zero or + // non-canonical denominator, which for the Lagrange batch means + // the squeezed x landed on a domain point). Both fail closed at + // the section boundary, but they are different incidents and used + // to surface under the same PrecompileFailed selector, pointing + // responders at the node when the transcript was the cause. + function batch_invert(success, mptr_start, mptr_end, scratch_mptr, r) -> ret, precompile_failed { ret := success if iszero(ret) { leave } // Memory ranges must be forward and word-aligned by @@ -142,6 +165,13 @@ // just run one modexp inverse in place. if eq(count_bytes, 0x20) { let x := mload(mptr_start) + // Reject anything congruent to zero mod r, not just the + // literal word 0: modexp would return 0 for those too, and + // the caller would take it for a valid inverse. + if iszero(lt(x, r)) { + ret := 0 + leave + } if iszero(x) { ret := 0 leave @@ -154,24 +184,43 @@ mstore(add(single_scratch, {{ template_constants.modexp.base_offset|hex() }}), x) mstore(add(single_scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(r, 2)) mstore(add(single_scratch, {{ template_constants.modexp.mod_offset|hex() }}), r) - ret := staticcall(gas(), {{ template_constants.modexp.address|hex() }}, single_scratch, {{ template_constants.modexp.frame_bytes|hex() }}, single_scratch, {{ template_constants.modexp.output_bytes|hex() }}) + ret := staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, single_scratch, {{ template_constants.modexp.frame_bytes|hex() }}, single_scratch, {{ template_constants.modexp.output_bytes|hex() }}) ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) + precompile_failed := iszero(ret) if ret { mstore(mptr_start, mload(single_scratch)) } leave } // Forward pass: scratch stores prefix products up to, but not // including, the final element. `gp` becomes the total product. + // + // Match the single-element path: reject non-canonical words + // (x >= r) instead of letting mulmod reduce them silently, so + // accept/reject semantics do not depend on batch length. let gp_mptr := scratch_mptr let gp := mload(mptr_start) + if iszero(lt(gp, r)) { + ret := 0 + leave + } let mptr := add(mptr_start, 0x20) for {} lt(mptr, sub(mptr_end, 0x20)) {} { - gp := mulmod(gp, mload(mptr), r) + let x := mload(mptr) + if iszero(lt(x, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x, r) mstore(gp_mptr, gp) mptr := add(mptr, 0x20) gp_mptr := add(gp_mptr, 0x20) } - gp := mulmod(gp, mload(mptr), r) + let x_last := mload(mptr) + if iszero(lt(x_last, r)) { + ret := 0 + leave + } + gp := mulmod(gp, x_last, r) // A zero total product means at least one denominator was // zero, so no batch inverse exists. if iszero(gp) { @@ -186,8 +235,15 @@ mstore(add(gp_mptr, {{ template_constants.modexp.base_offset|hex() }}), gp) mstore(add(gp_mptr, {{ template_constants.modexp.exp_offset|hex() }}), sub(r, 2)) mstore(add(gp_mptr, {{ template_constants.modexp.mod_offset|hex() }}), r) - ret := staticcall(gas(), {{ template_constants.modexp.address|hex() }}, gp_mptr, {{ template_constants.modexp.frame_bytes|hex() }}, gp_mptr, {{ template_constants.modexp.output_bytes|hex() }}) + ret := staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, gp_mptr, {{ template_constants.modexp.frame_bytes|hex() }}, gp_mptr, {{ template_constants.modexp.output_bytes|hex() }}) ret := and(ret, eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) + precompile_failed := iszero(ret) + // Leave before the backward pass on a failed modexp. A failed + // staticcall writes no output, so `mload(gp_mptr)` would read + // back the stale frame header and the pass below would + // overwrite every denominator in [mptr_start, mptr_end) with + // garbage products before returning ret = 0. + if iszero(ret) { leave } let all_inv := mload(gp_mptr) // Backward pass: derive each inverse from the inverted total @@ -212,7 +268,12 @@ // 4-word G1 slots; G2 bases are loaded from the pinned VK payload. function ec_pairing(success, lhs_mptr, rhs_mptr) -> ret { ret := success - if iszero(ret) { leave } + // Every other exit from this function reverts, and the + // terminal `return(RETURN_MPTR, 0x20)` in TraceReturn.yul + // returns true without consulting `success`. Revert here too, + // so this helper has no path that hands control back to a + // caller that would report success for an unverified proof. + if iszero(ret) { fail(ERR_PROOF_REJECTED) } // Lay out two (G1, G2) pairs at scratch..scratch+0x300: // [lhs_g1 (0x80) | G2_BASE (0x100) | rhs_g1 (0x80) | NEG_S_G2_BASE (0x100)] // Cancun MCOPY (3 + 3·words gas) replaces what used to @@ -224,9 +285,20 @@ mcopy(add(scratch, 0x80), G2_BASE_MPTR, 0x100) mcopy(add(scratch, 0x180), rhs_mptr, 0x80) mcopy(add(scratch, 0x200), NEG_S_G2_BASE_MPTR, 0x100) - ret := staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) + // MF-4: separate "the chain could not run the pairing" from + // "the pairing ran and rejected this proof". Both fail closed, + // but they are different incidents: the first points at the + // node/fork (a missing, repriced, or short-returning + // precompile), the second at the proof. Collapsing them into + // ProofRejected sent every responder looking at the wrong one. + ret := staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }}) ret := and(ret, eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) - ret := and(ret, mload(scratch)) - if iszero(ret) { revert(0, 0) } + if iszero(ret) { fail(ERR_PRECOMPILE_FAILED) } + // Compare against 1 rather than truncating to the low bit: + // `and(ret, word)` would accept any odd result word. EIP-2537 + // only ever returns 0 or 1, so this matches the strict form + // the constructor smoke test already uses. + ret := eq(mload(scratch), 1) + if iszero(ret) { fail(ERR_PROOF_REJECTED) } ret := 1 } diff --git a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol index 69b965f26..04a7d36cf 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/Constants.sol +++ b/proofs/solidity-verifier/templates/partials/verifier/Constants.sol @@ -120,10 +120,13 @@ uint256 internal constant Q_EVAL_CPTR_MPTR = {{ memory.q_eval_cptr_mptr }}; // Reserved 4-word slot for the G1 identity (point at infinity) in - // EIP-2537 padded form. EVM memory is zero-initialised, and we - // never write to this region, so the four `mload`s below produce - // 0,0,0,0 which is exactly the identity encoding the EIP-2537 - // ec_add / ec_mul precompiles accept. + // EIP-2537 padded form. EVM memory is zero-initialised, and the verifier + // never writes to this region, so any read of this slot (the PCS + // emitters `mcopy` from it when staging identity commitments) yields + // 0,0,0,0 -- exactly the identity encoding the EIP-2537 precompiles + // accept. Artifacts whose PCS plan never stages an identity commitment + // still emit the constant; it costs no runtime bytes beyond the + // declaration and keeps the emitters' pointer model uniform. uint256 internal constant G1_IDENTITY_MPTR = {{ memory.g1_identity_mptr }}; // Decoded polynomial-eval buffer (Optimisation H3). The off-chain @@ -136,6 +139,10 @@ uint256 internal constant SELECTOR_ACC_MPTR = {{ memory.selector_acc_mptr|hex() }}; uint256 internal constant QUOTIENT_RETURN_MPTR = {{ memory.quotient_return_mptr|hex() }}; uint256 internal constant BATCH_INV_SCRATCH_MPTR = {{ memory.batch_invert_scratch_mptr|hex() }}; + // Lagrange batch-inversion input run: denominators, in-place inverses, + // then Lagrange values, consumed and distilled into the named theta + // slots by the Lagrange block. Planner-registered phase scratch. + uint256 internal constant LAGRANGE_DENOMS_MPTR = {{ memory.lagrange_denoms_mptr|hex() }}; uint256 internal constant TRACE_U256_MPTR = {{ memory.trace_u256_mptr|hex() }}; // ---------------------------------------------------------------------- @@ -161,6 +168,72 @@ uint256 internal constant TRASHCAN_COMMS_MPTR_BASE = {{ memory.trashcan_comms_mptr_base }}; uint256 internal constant QUOTIENT_LIMB_COMMS_MPTR_BASE = {{ memory.quotient_limb_comms_mptr_base }}; + // ---------------------------------------------------------------------- + // Precompile gas bounds: the scheduled EIP-2537 costs, and for modexp the + // maximum over the EIP-2565 and EIP-7883 schedules. + // + // A failing EIP-2537 or modexp call consumes ALL gas supplied to the + // STATICCALL, so every generated call site forwards the exact scheduled + // cost instead of gas(). A malformed proof point then burns at most the + // scheduled cost of the single failing call instead of 63/64 of the + // transaction budget. The schedule is the spec-guaranteed worst case + // (EIP-2537 "DDoS protection" rationale), so these bounds are sufficient + // by construction on any conformant chain. + // + // MODEXP_GAS covers both live modexp schedules: EIP-2565 prices this + // frame at 1360, EIP-7883 (Osaka/Fusaka) removes the /3 divisor and + // prices it at 4080, so the larger bound is rendered. Forwarding the + // EIP-7883 bound on a pre-Osaka chain is free on success -- unused gas is + // returned -- while forwarding the EIP-2565 bound on a repriced chain + // reverts every proof. + // + // Liveness caveat: if a future fork reprices these precompiles above the + // bounds below, this verifier must be regenerated and redeployed. The + // constructor smoke probes forward the same bounds for EVERY precompile + // the runtime calls, modexp included, so deployment onto an + // already-repriced chain fails fast instead of bricking at proof time. + // ---------------------------------------------------------------------- + uint256 internal constant G1ADD_GAS = {{ template_constants.gas.g1add }}; + uint256 internal constant G1MSM_GAS_1PAIR = {{ template_constants.gas.g1msm_one_pair }}; + uint256 internal constant PAIRING_GAS_2PAIR = {{ template_constants.gas.pairing_two_pair }}; + uint256 internal constant MODEXP_GAS = {{ template_constants.gas.modexp }}; + // Exact cost of the deployment-time worst-case G1MSM smoke probe. + uint256 internal constant G1MSM_GAS_SMOKE = {{ constructor_g1msm_smoke_gas }}; + {%- if self.expected_has_accumulator %} + // Worst-case accumulator RHS MSM: carried RHS point plus every generated + // fixed-base tail scalar nonzero. Zero tail scalars are omitted at + // runtime, which only lowers the actual cost below this bound. + uint256 internal constant ACC_RHS_MSM_GAS = {{ acc_rhs_msm_gas }}; + {%- endif %} + + /// @notice Build identity for this generated artifact (P10/L-8). + /// @dev keccak256 over: the domain tag "halo2-solidity-verifier-build-v1", + /// the u64-length-prefixed generator feature profile, the vk_digest, + /// the expected VK runtime codehash (zero when the VK is embedded), + /// the SRS fingerprint keccak("halo2-solidity-verifier-srs-v1" || n + /// || G2 || s_g2 || [tau]G1), and an optional 32-byte deployment + /// provenance tag (0x00 marker when absent, 0x01 || tag when set). + /// The deployment record must publish these preimage components so + /// third parties can recompute the id; see + /// docs/reference/DEPLOYMENT_AND_INCIDENT_RESPONSE.md. + bytes32 public constant BUILD_ID = {{ build_id|hex_padded(64) }}; + + // ---------------------------------------------------------------------- + // Typed-error selectors (P4/L-3): bytes4(keccak256("Name()")) of the + // errors declared on the contract, as Yul-readable constants. The + // `fail(sel)` helper in AssemblyHelpers.yul writes the selector to + // scratch 0x00 and reverts with 4 bytes. Pinned by + // `p4_error_selectors_match_declared_errors` in src/lowering/tests.rs. + // ---------------------------------------------------------------------- + uint256 internal constant ERR_BAD_CALLDATA_SHAPE = 0x1b99e37c; + uint256 internal constant ERR_VK_MISMATCH = 0xa447d73e; + uint256 internal constant ERR_NON_CANONICAL_SCALAR = 0x77530042; + uint256 internal constant ERR_BAD_POINT_ENCODING = 0xf27905ec; + uint256 internal constant ERR_PRECOMPILE_FAILED = 0x84e81692; + uint256 internal constant ERR_PROOF_REJECTED = 0xc3b0d8cd; + uint256 internal constant ERR_QUOTIENT_PROGRAM_INVALID = 0x3cc81b89; + uint256 internal constant ERR_MEMORY_LAYOUT_VIOLATED = 0xc9888d23; + // BLS12-381 scalar-field modulus, used for transcript challenges and all // Halo2 verifier arithmetic. uint256 internal constant FR_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; diff --git a/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul index 2d27318c9..3d2a12a7c 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/FinalPairing.yul @@ -15,8 +15,11 @@ { let batch_ptr := {{ memory.accumulator_pairing_batch_mptr|hex() }} - // Domain || KZG rhs/lhs || accumulator rhs/lhs. + // Domain || vk_digest || KZG rhs/lhs || accumulator rhs/lhs. + // vk_digest makes alpha's binding to the verifying key local + // instead of transitive-through-the-points (audit I-7). mstore(batch_ptr, {{ template_constants.accumulator.pairing_batch_domain_tag_hex }}) + mstore(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_vk_digest_offset|hex() }}), mload(VK_DIGEST_MPTR)) mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_rhs_offset|hex() }}), PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_lhs_offset|hex() }}), PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mcopy(add(batch_ptr, {{ template_constants.accumulator.pairing_batch_acc_rhs_offset|hex() }}), ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) @@ -33,12 +36,12 @@ mcopy(batch_ptr, ACC_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) if success { - success := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) if success { - success := staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_RHS_MPTR, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } @@ -47,12 +50,12 @@ mcopy(batch_ptr, ACC_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) mstore(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), acc_pair_alpha) if success { - success := staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}, batch_ptr, {{ template_constants.g1_msm_pair_bytes|hex() }}, batch_ptr, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } mcopy(add(batch_ptr, {{ template_constants.g1_bytes|hex() }}), PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) if success { - success := staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) + success := staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}, batch_ptr, {{ template_constants.g1add_input_bytes|hex() }}, PAIRING_LHS_MPTR, {{ template_constants.g1_bytes|hex() }}) success := and(success, eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) } } @@ -75,7 +78,7 @@ // -- the historical "LHS"/"RHS" naming follows the dual MSM // accumulator (left = pi, right = combined) and *not* the // pairing argument order. Pass them swapped to ec_pairing. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_PRECOMPILE_FAILED) } success := ec_pairing(success, PAIRING_RHS_MPTR, PAIRING_LHS_MPTR) {%- if self.gas_checkpoints %} diff --git a/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul index 6eb05fce4..1a0b06fb2 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/Lagrange.yul @@ -1,6 +1,10 @@ // =============================================================== // Lagrange & instance-evaluation block (pure Fr arithmetic). // =============================================================== + // MF-4: hoisted so the section boundary below can tell a failed + // modexp (chain fault) from a rejected denominator (x landed on a + // domain point) instead of reporting both as PrecompileFailed. + let lagrange_precompile_failed := 0 { let k := {{ k }} let x := mload(X_MPTR) @@ -15,8 +19,10 @@ // First pass writes denominators (x - omega_i) for every // Lagrange value needed below, then appends x^n - 1. The // batch inversion pass turns all of them into inverses in one - // modexp call. - let mptr := X_N_MPTR + // modexp call. The run lives in the dedicated planner-registered + // LAGRANGE_DENOMS_MPTR scratch region; only the distilled + // results below are persisted into the named theta slots. + let mptr := LAGRANGE_DENOMS_MPTR let mptr_end := add(mptr, {{ ((num_instances + num_neg_lagranges) * 32)|hex() }}) {%- if num_instances == 0 %} // No public instances still need one denominator slot so @@ -31,11 +37,11 @@ } let x_n_minus_1 := addmod(x_n, sub(r, 1), r) mstore(mptr_end, x_n_minus_1) - success := batch_invert(success, X_N_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) + success, lagrange_precompile_failed := batch_invert(success, LAGRANGE_DENOMS_MPTR, add(mptr_end, 0x20), BATCH_INV_SCRATCH_MPTR, r) // Convert inverted denominators into Lagrange evaluations: // L_i(x) = (x^n - 1) * n^-1 * omega_i / (x - omega_i). - mptr := X_N_MPTR + mptr := LAGRANGE_DENOMS_MPTR let l_i_common := mulmod(x_n_minus_1, mload(N_INV_MPTR), r) for { let pow_of_omega := mload(OMEGA_INV_TO_L_MPTR) } lt(mptr, mptr_end) @@ -46,9 +52,9 @@ // l_blind is the sum of the negative-rotation Lagrange terms // used by the midnight-proofs blinding identity. - let l_blind := mload(add(X_N_MPTR, 0x20)) - let l_i_cptr := add(X_N_MPTR, 0x40) - for { let l_i_cptr_end := add(X_N_MPTR, {{ (num_neg_lagranges * 32)|hex() }}) } + let l_blind := mload(add(LAGRANGE_DENOMS_MPTR, 0x20)) + let l_i_cptr := add(LAGRANGE_DENOMS_MPTR, 0x40) + for { let l_i_cptr_end := add(LAGRANGE_DENOMS_MPTR, {{ (num_neg_lagranges * 32)|hex() }}) } lt(l_i_cptr, l_i_cptr_end) { l_i_cptr := add(l_i_cptr, 0x20) } { l_blind := addmod(l_blind, mload(l_i_cptr), r) @@ -71,8 +77,8 @@ // Persist the derived values into named memory slots consumed // by quotient reconstruction and PCS preparation. let x_n_minus_1_inv := mload(mptr_end) - let l_last := mload(X_N_MPTR) - let l_0 := mload(add(X_N_MPTR, {{ (num_neg_lagranges * 32)|hex() }})) + let l_last := mload(LAGRANGE_DENOMS_MPTR) + let l_0 := mload(add(LAGRANGE_DENOMS_MPTR, {{ (num_neg_lagranges * 32)|hex() }})) mstore(X_N_MPTR, x_n) mstore(X_N_MINUS_1_INV_MPTR, x_n_minus_1_inv) @@ -86,4 +92,10 @@ gas_checkpoint(11) // after Lagrange + instance evaluation block {%- endif %} - if iszero(success) { revert(0, 0) } + if iszero(success) { + // A zero or non-canonical denominator is a rejected input, + // not a broken chain: the only way to reach it is a squeezed + // x that coincides with a domain point (probability ~n/r). + if lagrange_precompile_failed { fail(ERR_PRECOMPILE_FAILED) } + fail(ERR_PROOF_REJECTED) + } diff --git a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol index baa6e6180..1545e1b0e 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol +++ b/proofs/solidity-verifier/templates/partials/verifier/PrecompileSmoke.sol @@ -1,7 +1,26 @@ - /// @notice Smoke-check the Cancun/EIP-2537 runtime features required by the verifier. - /// @dev Exercises MCOPY and identity EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// @notice Smoke-check the Cancun/EIP-2537/modexp runtime features required by the verifier. + /// @dev Exercises MCOPY, modexp, and EIP-2537 inputs to catch incompatible chain/fork configurations at deployment. + /// The probes forward the same exact gas bounds the runtime uses, for + /// every precompile it calls -- 0x05 modexp included (see the + /// gas-bound constants block) -- so a chain whose precompile schedule + /// was repriced above those bounds fails here, at deployment, instead + /// of bricking verifyProof later. function require_eip2537_precompiles() private view { assembly ("memory-safe") { + // Same free-memory-pointer guard as verifyProof. This body runs in + // the *creation* frame, which the generator's memoryguard test does + // not inspect (it parses the runtime prologue only). + // + // MF-2: typed like the runtime guard, and for a stronger reason. + // The runtime guard turns a bad recompile into a revert on every + // proof; this one turns it into a failed DEPLOYMENT, which is + // where a build fault belongs. The probes below keep bare reverts + // (a chain-capability failure, not a build fault). + if gt(mload(0x40), {{ memory.constructor_smoke_scratch_mptr|hex() }}) { + mstore(0x00, shl(224, ERR_MEMORY_LAYOUT_VIOLATED)) + revert(0x00, 0x04) + } + // Scratch is reused for every runtime-prerequisite probe. let scratch := {{ memory.constructor_smoke_scratch_mptr|hex() }} @@ -12,8 +31,39 @@ mcopy(add(scratch, {{ template_constants.word_bytes|hex() }}), scratch, {{ template_constants.word_bytes|hex() }}) if iszero(eq(mload(add(scratch, {{ template_constants.word_bytes|hex() }})), 0x1234)) { revert(0, 0) } + // ---------------------------------------------------------------- + // modexp (0x05) known-answer probe at the pinned runtime bound. + // + // MF-1: every other precompile the runtime calls was probed here, + // but modexp -- which the MANDATORY Lagrange batch inversion and + // every scalar_inv call depend on -- was not. Two live schedules + // price this frame differently (EIP-2565: 1360, EIP-7883: 4080), + // and a bound below the chain's price does not degrade: the + // staticcall forwards a fixed amount, the precompile OOGs, and + // EVERY proof reverts PrecompileFailed. Without this probe that + // failure is invisible until the first verifyProof call, on a + // contract that deployed cleanly. + // + // The vector is the runtime's own operation -- Fermat inversion + // in Fr -- so it exercises the exact frame shape, exponent width, + // and gas bound used at proof time: 2^(FR_MODULUS - 2) == 2^-1. + // Checking mulmod(result, 2, FR_MODULUS) == 1 rather than a + // rendered constant keeps the probe self-contained while still + // rejecting a stub: a precompile returning zeros (or its input) + // fails, since 0 * 2 != 1 mod r. + // ---------------------------------------------------------------- + mstore(add(scratch, {{ template_constants.modexp.base_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // base len + mstore(add(scratch, {{ template_constants.modexp.exp_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // exp len + mstore(add(scratch, {{ template_constants.modexp.mod_len_offset|hex() }}), {{ template_constants.word_bytes|hex() }}) // mod len + mstore(add(scratch, {{ template_constants.modexp.base_offset|hex() }}), 2) + mstore(add(scratch, {{ template_constants.modexp.exp_offset|hex() }}), sub(FR_MODULUS, 2)) + mstore(add(scratch, {{ template_constants.modexp.mod_offset|hex() }}), FR_MODULUS) + if iszero(staticcall(MODEXP_GAS, {{ template_constants.modexp.address|hex() }}, scratch, {{ template_constants.modexp.frame_bytes|hex() }}, scratch, {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.modexp.output_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(mulmod(mload(scratch), 2, FR_MODULUS), 1)) { revert(0, 0) } + // Start the EIP-2537 probes with the identity encoding for G1/G2: - // all-zero padded words. + // all-zero padded words. This also clears the modexp frame above. for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { mstore(add(scratch, off), 0) } @@ -21,23 +71,144 @@ // G1ADD(identity, identity) -> identity, 128-byte return. // This catches chains where the precompile is missing or returns a // non-standard success shape. - if iszero(staticcall(gas(), {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) } + // Known-answer probe: G1ADD(G, G) == 2G. + // + // Every probe above uses the point at infinity, which is exactly + // the input an implementation gets right without doing any curve + // arithmetic -- a precompile that returns its zero-filled input, or + // zeros for anything, satisfies them. The identity is also the one + // input on which an implementation that omits the EIP-2537 subgroup + // check still answers correctly, and the production verifier leans + // on G1MSM as its subgroup validator for absorbed commitments. So + // add one vector whose answer a stub cannot guess. + mstore(add(scratch, 0x00), {{ template_constants.eip2537.g1_generator.0|hex_padded(64) }}) + mstore(add(scratch, 0x20), {{ template_constants.eip2537.g1_generator.1|hex_padded(64) }}) + mstore(add(scratch, 0x40), {{ template_constants.eip2537.g1_generator.2|hex_padded(64) }}) + mstore(add(scratch, 0x60), {{ template_constants.eip2537.g1_generator.3|hex_padded(64) }}) + mcopy(add(scratch, {{ template_constants.g1_bytes|hex() }}), scratch, {{ template_constants.g1_bytes|hex() }}) + if iszero(staticcall(G1ADD_GAS, {{ template_constants.eip2537.g1add_address|hex() }}, scratch, {{ template_constants.g1add_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), {{ template_constants.eip2537.g1_double_generator.0|hex_padded(64) }}), + eq(mload(add(scratch, 0x20)), {{ template_constants.eip2537.g1_double_generator.1|hex_padded(64) }}) + ), + and( + eq(mload(add(scratch, 0x40)), {{ template_constants.eip2537.g1_double_generator.2|hex_padded(64) }}), + eq(mload(add(scratch, 0x60)), {{ template_constants.eip2537.g1_double_generator.3|hex_padded(64) }}) + ) + )) { revert(0, 0) } + + + // ---------------------------------------------------------------- + // Known-answer probes for the two precompiles that actually decide + // acceptance. + // + // Every probe above this point uses the point at infinity or a + // G1ADD vector. That leaves the two precompiles the verifier's + // security actually rests on untested for *rejection* behaviour: + // - 0x0c G1MSM is the curve/subgroup validator for every absorbed + // proof commitment (common_uncompressed_g1 runs no curve check); + // - 0x0f PAIRING_CHECK is the sole accept gate, so a chain whose + // 0x0f always returns 1 accepts every proof. + // These four probes cost deployment gas only. + // ---------------------------------------------------------------- + + // (a) G1MSM known answer: [2]*G == 2G. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x20), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x40), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x60), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x80), 2) + if iszero(staticcall(G1MSM_GAS_1PAIR, {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, 0xa0, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(and( + and( + eq(mload(add(scratch, 0x00)), 0x000000000000000000000000000000000572cbea904d67468808c8eb50a9450c), + eq(mload(add(scratch, 0x20)), 0x9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e) + ), + and( + eq(mload(add(scratch, 0x40)), 0x00000000000000000000000000000000166a9d8cabc673a322fda673779d8e38), + eq(mload(add(scratch, 0x60)), 0x22ba3ecb8670e461f73bb9021d5fd76a4c56d9d4cd16bd1bba86881979749d28) + ) + )) { revert(0, 0) } + + // (b) G1MSM negative probe. (4, y) satisfies y^2 = x^3 + 4 over Fp + // but is NOT in the r-order subgroup (checked off-chain: r*P != O). + // EIP-2537 requires G1MSM to reject it. This is the one property + // the verifier's deferred-validation strategy depends on and the + // one property no other probe exercises. + // + // Gas is bounded on purpose: a precompile that rejects its input + // consumes everything forwarded to it, so an unbounded `gas()` here + // would burn 63/64 of the deployment gas before the probes below. + mstore(add(scratch, 0x00), 0x0000000000000000000000000000000000000000000000000000000000000000) + mstore(add(scratch, 0x20), 0x0000000000000000000000000000000000000000000000000000000000000004) + mstore(add(scratch, 0x40), 0x000000000000000000000000000000000a989badd40d6212b33cffc3f3763e9b) + mstore(add(scratch, 0x60), 0xc760f988c9926b26da9dd85e928483446346b8ed00e1de5d5ea93e354abe706c) + mstore(add(scratch, 0x80), 1) + if staticcall(200000, {{ template_constants.eip2537.g1msm_address|hex() }}, scratch, 0xa0, scratch, {{ template_constants.g1_bytes|hex() }}) { revert(0, 0) } + + // (c)+(d) Pairing known answers. Lay out [G1 | G2 | G1' | G2] once: + // with G1' = -G the product is 1, with G1' = +G it is not. G2 is + // written literally because the VK payload is not loaded during + // construction. + mstore(add(scratch, 0x000), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x020), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x040), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x060), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + mstore(add(scratch, 0x080), 0x00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051) + mstore(add(scratch, 0x0a0), 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8) + mstore(add(scratch, 0x0c0), 0x0000000000000000000000000000000013e02b6052719f607dacd3a088274f65) + mstore(add(scratch, 0x0e0), 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e) + mstore(add(scratch, 0x100), 0x000000000000000000000000000000000ce5d527727d6e118cc9cdc6da2e351a) + mstore(add(scratch, 0x120), 0xadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801) + mstore(add(scratch, 0x140), 0x000000000000000000000000000000000606c4a02ea734cc32acd2b02bc28b99) + mstore(add(scratch, 0x160), 0xcb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be) + mstore(add(scratch, 0x180), 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f) + mstore(add(scratch, 0x1a0), 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb) + mstore(add(scratch, 0x1c0), 0x00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f2) + mstore(add(scratch, 0x1e0), 0x67816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca) + mcopy(add(scratch, 0x200), add(scratch, 0x80), 0x100) + + // (c) e(G, G2) * e(-G, G2) == 1. + if iszero(staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, add(scratch, 0x300), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(mload(add(scratch, 0x300)), 1)) { revert(0, 0) } + + // (d) e(G, G2) * e(G, G2) != 1. Flip the second G1 back to +G. + mstore(add(scratch, 0x1c0), 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4) + mstore(add(scratch, 0x1e0), 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1) + if iszero(staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, add(scratch, 0x300), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(iszero(mload(add(scratch, 0x300)))) { revert(0, 0) } + + // Restore the identity encoding for the probes below. + for { let off := 0 } lt(off, {{ template_constants.eip2537.smoke_scratch_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { + mstore(add(scratch, off), 0) + } + // Worst-case generated G1MSM with all identity/zero terms -> // identity, 128-byte return. This exercises the largest MSM input - // length rendered by this verifier instead of only a one-pair - // smoke call. + // LENGTH rendered by this verifier instead of only a one-pair + // smoke call, proving the target chain's precompile accepts the + // full-size input. It runs in the creation frame at its own + // scratch base, so it does not (and cannot) pre-expand the + // runtime call frame's memory -- constructor memory is discarded; + // only the input size coverage carries over. let msm_scratch := {{ memory.constructor_g1msm_smoke_scratch_mptr|hex() }} for { let off := 0 } lt(off, {{ constructor_g1msm_smoke_input_bytes|hex() }}) { off := add(off, {{ template_constants.word_bytes|hex() }}) } { mstore(add(msm_scratch, off), 0) } // The production verifier uses G1MSM both for commitments and as // the subgroup validator for absorbed proof points. - if iszero(staticcall(gas(), {{ template_constants.eip2537.g1msm_address|hex() }}, msm_scratch, {{ constructor_g1msm_smoke_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(G1MSM_GAS_SMOKE, {{ template_constants.eip2537.g1msm_address|hex() }}, msm_scratch, {{ constructor_g1msm_smoke_input_bytes|hex() }}, scratch, {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.g1_bytes|hex() }})) { revert(0, 0) } if or(or(mload(scratch), mload(add(scratch, 0x20))), or(mload(add(scratch, 0x40)), mload(add(scratch, 0x60)))) { revert(0, 0) @@ -47,7 +218,7 @@ // -> true, 32-byte return. This matches the runtime two-pair KZG // pairing input size and catches absent pairing precompiles, // short return data, and obviously incompatible semantics. - if iszero(staticcall(gas(), {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } + if iszero(staticcall(PAIRING_GAS_2PAIR, {{ template_constants.eip2537.pairing_address|hex() }}, scratch, {{ template_constants.pairing_two_pair_bytes|hex() }}, scratch, {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } if iszero(eq(returndatasize(), {{ template_constants.word_bytes|hex() }})) { revert(0, 0) } if iszero(eq(mload(scratch), 1)) { revert(0, 0) } } diff --git a/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul index a8d59eb26..062f59c61 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/QuotientAndLinearization.yul @@ -21,16 +21,21 @@ if iszero(and( eq(extcodesize(quotientEvaluator), EXPECTED_QUOTIENT_LENGTH), eq(extcodehash(quotientEvaluator), EXPECTED_QUOTIENT_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } {%- when None %} {%- endmatch %} {%- if self.trace %} - if iszero(call(gas(), quotientEvaluator, 0, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { revert(0, 0) } + if iszero(call(gas(), quotientEvaluator, 0, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } {%- else %} - if iszero(staticcall(gas(), quotientEvaluator, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { revert(0, 0) } + // gas() forwarding is deliberate here, unlike the precompile + // call sites: this is a regular contract call, so a reverting + // or failing callee refunds its unused gas -- only precompile + // ERRORS burn everything forwarded (EIP-2537). The callee is + // also pinned by codehash above, not attacker-supplied. + if iszero(staticcall(gas(), quotientEvaluator, {{ qext.frame_base|hex() }}, {{ qext.frame_len|hex() }}, q_out, {{ qext.output_len|hex() }})) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } {%- endif %} - if iszero(eq(returndatasize(), {{ qext.output_len|hex() }})) { revert(0, 0) } - if iszero(eq(mload(q_out), {{ qext.magic|hex_padded(64) }})) { revert(0, 0) } + if iszero(eq(returndatasize(), {{ qext.output_len|hex() }})) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } + if iszero(eq(mload(q_out), {{ qext.magic|hex_padded(64) }})) { fail(ERR_QUOTIENT_PROGRAM_INVALID) } // Word 1 is the negated y-batched identity numerator, stored // in the same memory slot used by the monolithic path. mstore(QUOTIENT_EVAL_MPTR, mload(add(q_out, 0x20))) diff --git a/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul b/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul index 5f040942c..aa27b06c6 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/TraceReturn.yul @@ -54,5 +54,11 @@ // Success path is terminal. Invalid inputs have already reverted, // so the Solidity ABI observes `true`. + // + // The guard is redundant today -- every failure path above reverts + // rather than clearing `success` -- but it keeps acceptance a local + // property of this file instead of an invariant split across + // FinalPairing.yul and ec_pairing. + if iszero(success) { fail(ERR_PROOF_REJECTED) } mstore(RETURN_MPTR, 1) return(RETURN_MPTR, 0x20) diff --git a/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul index 80e97e1cc..0561aaf53 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/TranscriptProofParser.yul @@ -65,7 +65,7 @@ // Keccak Fq transcript input. buf_len := common_word(buf_len, inst_be) } - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } } {%- if self.gas_checkpoints %} @@ -316,7 +316,7 @@ // Proof evaluation scalars must be canonical Fr elements // before they are absorbed or made available to quotient // reconstruction. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } // Spill for quotient numerator and PCS codegen. mstore(eval_buf, eval) eval_buf := add(eval_buf, {{ template_constants.word_bytes|hex() }}) @@ -379,7 +379,7 @@ {} { let eval := calldataload(proof_cptr) // Canonical Fr check before transcript absorption. - if iszero(lt(eval, r)) { revert(0, 0) } + if iszero(lt(eval, r)) { fail(ERR_NON_CANONICAL_SCALAR) } buf_len := common_word(buf_len, eval) {%- if self.trace %} trace_u256(proof_eval_trace_id, eval) @@ -413,11 +413,11 @@ // NUM_INSTANCE_CPTR is the calldata word immediately after the // dynamic proof bytes payload. If proof_cptr lands anywhere else, // some section was under-read or over-read. - if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { revert(0, 0) } + if iszero(eq(proof_cptr, NUM_INSTANCE_CPTR)) { fail(ERR_BAD_CALLDATA_SHAPE) } // `success` carries deferred canonicality failures from public // instance reads. G1/proof scalar helpers revert immediately. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_NON_CANONICAL_SCALAR) } {%- if self.gas_checkpoints %} gas_checkpoint(10) // after evaluations + x1/x2 + f_com + x3 + q_evals + x4 + pi (transcript done) diff --git a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul index 35aca7478..bd522d101 100644 --- a/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul +++ b/proofs/solidity-verifier/templates/partials/verifier/VkLoading.yul @@ -67,7 +67,7 @@ if iszero(and( eq(extcodesize(vk), EXPECTED_VK_LENGTH), eq(extcodehash(vk), EXPECTED_VK_CODEHASH_WORD) - )) { revert(0, 0) } + )) { fail(ERR_VK_MISMATCH) } // Runtime byte 0 is INVALID so direct calls cannot execute the // payload. Copy from byte 1 into VK_MPTR to reconstruct the // exact payload layout used by the embedded branch. @@ -85,7 +85,7 @@ success := and(success, eq(mload(ACC_OFFSET_MPTR), {{ expected_acc_offset }})) success := and(success, eq(mload(NUM_ACC_LIMBS_MPTR), {{ expected_num_acc_limbs }})) success := and(success, eq(mload(NUM_ACC_LIMB_BITS_MPTR), {{ expected_num_acc_limb_bits }})) - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_VK_MISMATCH) } // // The checks below validate the dynamic ABI envelope before the // transcript parser starts walking raw calldata: @@ -109,7 +109,7 @@ ) // Stop before any transcript absorption if the ABI/proof shape // is not exactly the generated one. - if iszero(success) { revert(0, 0) } + if iszero(success) { fail(ERR_BAD_CALLDATA_SHAPE) } } {%- if self.expected_has_accumulator %} @@ -126,8 +126,14 @@ // validate_public_accumulator returns a boolean to share the same // success-plumbing style as other helper calls; this boundary is // where the verifier converts failure to a revert. - success := validate_public_accumulator(success, r) - if iszero(success) { revert(0, 0) } + let acc_precompile_failed := 0 + success, acc_precompile_failed := validate_public_accumulator(success, r) + if iszero(success) { + // MF-4: a G1MSM that could not run at all is a chain fault, + // not a malformed accumulator point. + if acc_precompile_failed { fail(ERR_PRECOMPILE_FAILED) } + fail(ERR_BAD_POINT_ENCODING) + } {%- endif %} {%- if self.gas_checkpoints %} diff --git a/proofs/solidity-verifier/tests/hybrid_mt_fixture.rs b/proofs/solidity-verifier/tests/hybrid_mt_fixture.rs index 4a94bbdd7..baaa8da6d 100644 --- a/proofs/solidity-verifier/tests/hybrid_mt_fixture.rs +++ b/proofs/solidity-verifier/tests/hybrid_mt_fixture.rs @@ -193,13 +193,17 @@ fn hybrid_mt_renders_compiles_and_verifies() { let srs_dir = srs_dir(); let srs_path = format!("{srs_dir}/bls_filecoin_2p{K}"); let fallback_srs_path = format!("{srs_dir}/bls_filecoin_2p19"); - if !Path::new(&srs_path).exists() && !Path::new(&fallback_srs_path).exists() { - eprintln!( - "skipping hybrid MT Solidity smoke: SRS not found at {srs_path} or \ - {fallback_srs_path}. Set SRS_DIR or fetch the asset under midfall/zk_stdlib." - ); - return; - } + // The gate was explicitly requested, so a missing asset fails rather than + // silently reporting a pass that rendered and compiled nothing. + assert!( + Path::new(&srs_path).exists() || Path::new(&fallback_srs_path).exists(), + "{RUN_EVM_TESTS_ENV}=1 requires the test SRS, but it was not found at {srs_path} or \ + {fallback_srs_path}. +Fetch it with: + curl -L -o {fallback_srs_path} \ + https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19 +or point SRS_DIR at an existing copy." + ); env::set_var("SRS_DIR", &srs_dir); let relation = HybridMtCircuit; @@ -263,10 +267,13 @@ fn hybrid_mt_renders_compiles_and_verifies() { verifier_solidity.len() ); - if !pinned_solc_available() { - eprintln!("skipping hybrid MT EVM smoke: pinned solc not available"); - return; - } + assert!( + pinned_solc_available(), + "{RUN_EVM_TESTS_ENV}=1 requires the pinned solc, which was not found or did not match. +\ + Install it, point SOLC at the binary, or set \ + HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 to accept another version." + ); let vk_creation_code = compile_solidity(&vk_solidity); let verifier_creation_code = compile_solidity(&verifier_solidity); diff --git a/proofs/solidity-verifier/tests/ivc_accumulator_replay.rs b/proofs/solidity-verifier/tests/ivc_accumulator_replay.rs new file mode 100644 index 000000000..d225ce495 --- /dev/null +++ b/proofs/solidity-verifier/tests/ivc_accumulator_replay.rs @@ -0,0 +1,689 @@ +// SPDX-License-Identifier: CC0-1.0 +//! CI-runnable adversarial replay of the public-accumulator decode path. +//! +//! The accumulator decoder in +//! `templates/partials/verifier/AccumulatorHelpers.yul` had no executing test +//! coverage. The tests that looked like they covered it +//! -- `accumulator_decoder_rejects_noncanonical_infinity` and friends in +//! `src/lowering/tests.rs` -- are `verifier_template.contains("...")` string +//! greps over the raw template. They assert the guard *text* exists and never +//! render, compile, or run it. That is why the always-false `and` guard in +//! `load_acc_coord_shifted` survived: those greps passed for as long as the +//! identity branch was dead code. +//! +//! The only executing accumulator tests live in `tests/ivc_keccak_solidity.rs`, +//! which proves a k=20 decider from scratch and needs ~300 MB of SRS, so it is +//! gated behind `HALO2_SOLIDITY_RUN_IVC_BENCH=1` and never runs in CI. +//! +//! This test closes that gap by replaying *pre-rendered* artifacts. Because it +//! ships the generated Solidity and the matching calldata rather than a +//! verifying key, it needs neither SRS nor a proving run nor +//! `midnight-aggregation` -- only solc and revm. A verifier cannot be rendered +//! from a VK without the full SRS (`SolidityGenerator` consumes +//! `params.g_lagrange()`), which is what rules out a vk.bin-based replay. +//! +//! Two fixtures are replayed, covering both accumulator encodings: +//! +//! - `fixtures/ivc` -- the IVC Keccak decider, `AccumulatorEncoding::new`, +//! which carries explicit lhs/rhs scalars. +//! - `fixtures/moonlight-wrap` -- the Moonlight wrap decider, `point_pair`, +//! which does not. Its `expected_acc_has_carried_scalars = false` arms were +//! previously only ever compiled, never executed against a proof. +//! +//! Each fixture's README records its provenance and regeneration command. +//! +//! A fixture describes itself: the accumulator offset, limb count and +//! `has_accumulator` flag are parsed back out of the rendered verifying-key +//! payload, the encoding kind is recovered from the payload width, and the +//! infinity encoding comes from the verifier's own constants. So this file +//! carries no per-fixture constants that could drift from the artifacts. +//! +//! Staleness caveat: these are snapshots of the codegen that produced them. +//! A fixture is self-consistent, so the replay keeps passing after a codegen +//! change -- it just stops testing current output. The commit stamp in each +//! README makes drift auditable; detecting it automatically would require +//! re-rendering, which needs the SRS again. + +#![cfg(feature = "evm")] + +use std::path::PathBuf; + +use halo2_solidity_verifier::{compile_solidity_with_runs, CallOutcome, Evm}; + +/// The IVC verifier is large, so it is rendered and benched at `runs = 1`. +const SOLC_OPTIMIZE_RUNS: u32 = 1; +/// Generous cap so an unexpected loop reports OutOfGas rather than masquerading +/// as a revert. +const GAS_CAP: u64 = 5_000_000_000; +/// ABI prologue: selector, proof head, instances head, then the proof length +/// word. The proof payload starts immediately after. +const PROOF_PAYLOAD_START: usize = 4 + 0x40 + 0x20; + +fn fixture_dir(fixture: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures").join(fixture) +} + +fn read_fixture(fixture: &str, name: &str) -> Vec { + let path = fixture_dir(fixture).join(name); + std::fs::read(&path).unwrap_or_else(|err| panic!("missing fixture {}: {err}", path.display())) +} + +fn read_fixture_string(fixture: &str, name: &str) -> String { + String::from_utf8(read_fixture(fixture, name)).expect("fixture should be UTF-8") +} + +/// Read an optional fixture file. Split renders ship a quotient evaluator; +/// single-contract renders do not. +fn read_optional_fixture_string(fixture: &str, name: &str) -> Option { + let path = fixture_dir(fixture).join(name); + std::fs::read_to_string(path).ok() +} + +/// Read a labelled VK payload word out of the rendered verifying-key source. +/// +/// The generator emits each header word as +/// `mstore(add(payload, 0x...), 0x...) // name`, so the fixture describes its +/// own accumulator placement and this test does not have to carry a second +/// copy that could drift out of sync with the artifact. +fn vk_payload_word(vk_solidity: &str, name: &str) -> u64 { + let suffix = format!("// {name}"); + let line = vk_solidity + .lines() + .map(str::trim) + .find(|line| line.starts_with("mstore(") && line.ends_with(&suffix)) + .unwrap_or_else(|| panic!("verifying-key source has no `{name}` payload word")); + let value = line + .rsplit_once("0x") + .expect("payload word should be hex") + .1 + .split_whitespace() + .next() + .expect("payload word should have a value") + .trim_end_matches(')'); + u64::from_str_radix(value.trim_start_matches('0'), 16).unwrap_or(0) +} + +fn read_u256_word(calldata: &[u8], offset: usize) -> u64 { + let word = &calldata[offset..offset + 0x20]; + assert!( + word[..24].iter().all(|b| *b == 0), + "word at {offset:#x} does not fit in u64" + ); + u64::from_be_bytes(word[24..].try_into().unwrap()) +} + +fn assert_reverts(outcome: CallOutcome, case: &str) -> u64 { + match outcome { + CallOutcome::Revert { gas_used, .. } => gas_used, + CallOutcome::Success { output, .. } => panic!( + "{case}: verifier accepted a proof it must reject (output = 0x{})", + hex::encode(output) + ), + CallOutcome::Halt { reason, .. } => { + panic!("{case}: expected a revert but the call halted ({reason})") + } + } +} + +/// Assert a revert that happened *before* the transcript was built. +/// +/// Every accumulator word is also absorbed into the Keccak transcript, so any +/// mutation inside the instance region changes the challenges and would fail at +/// the pairing even if its dedicated decoder guard were deleted. Anchoring on +/// gas distinguishes the two: `validate_public_accumulator` runs before the +/// transcript, so a guard that fires costs a small fraction of a full run. +/// Without this, a deleted guard would leave the test passing for the wrong +/// reason. +fn assert_reverts_before_transcript(outcome: CallOutcome, accepted_gas: u64, case: &str) { + let gas_used = assert_reverts(outcome, case); + let ceiling = accepted_gas / 2; + assert!( + gas_used < ceiling, + "{case}: reverted after {gas_used} gas, but an accumulator decode guard should \ + fire before the transcript (under {ceiling}, vs {accepted_gas} for a full \ + accepted run). This revert came from a later stage, so the guard under test \ + may no longer be reachable." + ); +} + +/// Assert a revert that happened only *after* the full verification ran. +/// +/// This is the signature of a Fiat-Shamir binding failure: the input was +/// structurally valid, so every range, packing, and framing check passed and +/// the proof failed at the final pairing. A cheap revert here would mean some +/// earlier check rejected the input instead, which proves nothing about +/// binding. +fn assert_reverts_at_pairing(outcome: CallOutcome, accepted_gas: u64, case: &str) { + let gas_used = assert_reverts(outcome, case); + let floor = accepted_gas / 4 * 3; + assert!( + gas_used > floor, + "{case}: reverted after only {gas_used} gas (expected over {floor}, near the \ + {accepted_gas} of a full accepted run). An early check rejected this input, so \ + it does not exercise instance binding." + ); +} + +/// The IVC decider carries explicit lhs/rhs scalars +/// (`AccumulatorEncoding::new`). +#[test] +fn ivc_accumulator_decoder_rejects_malformed_public_accumulator() { + replay_accumulator_fixture("ivc"); +} + +/// The Moonlight wrap decider uses the scalar-free `point_pair` encoding, whose +/// `expected_acc_has_carried_scalars = false` arms were previously only ever +/// compiled, never executed against a proof. +#[test] +fn wrap_point_pair_decoder_rejects_malformed_public_accumulator() { + replay_accumulator_fixture("moonlight-wrap"); +} + +/// Replay a rendered accumulator fixture, then mutate the proof and public +/// inputs and assert every mutation is rejected. +/// +/// The accept baseline is what gives the rejections meaning: without it a +/// "rejects" assertion could pass because the verifier rejects everything. +fn replay_accumulator_fixture(fixture: &str) { + let verifier_solidity = read_fixture_string(fixture, "Halo2Verifier.sol"); + let vk_solidity = read_fixture_string(fixture, "Halo2VerifyingKey.sol"); + let calldata = read_fixture(fixture, "calldata.bin"); + + assert_eq!( + vk_payload_word(&vk_solidity, "has_accumulator"), + 1, + "fixture must be a public-accumulator render, otherwise this test \ + exercises none of AccumulatorHelpers.yul" + ); + let final_acc_offset = vk_payload_word(&vk_solidity, "acc_offset") as usize; + let num_acc_limbs = vk_payload_word(&vk_solidity, "num_acc_limbs"); + + // Derive the accumulator's calldata position from the ABI layout; the + // fixture describes its own instance-space offset via the VK payload. + let proof_len = read_u256_word(&calldata, PROOF_PAYLOAD_START - 0x20) as usize; + let instances_len_word = PROOF_PAYLOAD_START + proof_len; + let instance_count = read_u256_word(&calldata, instances_len_word); + let first_acc_word = instances_len_word + 0x20 + final_acc_offset * 0x20; + // 7 limbs of 56 bits pack 4 to a field element, so each coordinate takes 2 + // words and each point 4. + assert_eq!( + num_acc_limbs, 7, + "limb count changed; the word arithmetic below no longer holds" + ); + + // Recover the encoding kind from the payload width rather than hardcoding + // it per fixture: the accumulator occupies the instance tail, so eight + // words is `point_pair` and ten is the scalar-carrying encoding. + let acc_words = instance_count as usize - final_acc_offset; + let has_carried_scalars = match acc_words { + 8 => false, + 10 => true, + other => panic!( + "unexpected accumulator payload of {other} words; expected 8 (point_pair) \ + or 10 (point-and-scalar)" + ), + }; + let scalar_stride = if has_carried_scalars { 0x20 } else { 0 }; + let lhs_scalar_word = first_acc_word + 4 * 0x20; + let rhs_first_word = lhs_scalar_word + scalar_stride; + let rhs_scalar_word = rhs_first_word + 4 * 0x20; + let acc_end_word = rhs_scalar_word + scalar_stride; + assert!( + acc_end_word <= calldata.len(), + "accumulator words run past the fixture calldata; the fixture is inconsistent" + ); + + // Guard the offset arithmetic above. Without this, a miscomputed + // `first_acc_word` would still make every mutation below revert -- for the + // wrong reason -- and the test would pass while exercising nothing. + // + // Four 56-bit limbs occupy the low 224 bits of a packed word, so the top + // four bytes of every accumulator coordinate word must be zero. Random + // proof or instance bytes would not satisfy this. + for (index, word_start) in [first_acc_word, rhs_first_word] + .into_iter() + .flat_map(|point| (0..4).map(move |w| point + w * 0x20)) + .enumerate() + { + assert_eq!( + &calldata[word_start..word_start + 4], + &[0u8; 4], + "accumulator coordinate word {index} at {word_start:#x} has non-zero high bytes; \ + the computed accumulator offset does not point at packed limbs" + ); + } + + let mut evm = Evm::default(); + let vk_address = evm.create(compile_solidity_with_runs(&vk_solidity, SOLC_OPTIMIZE_RUNS)); + let verifier_code = compile_solidity_with_runs(&verifier_solidity, SOLC_OPTIMIZE_RUNS); + // Split renders pin a separately deployed quotient evaluator; single + // contract renders take the verifying key alone. + let verifier_address = match read_optional_fixture_string(fixture, "Halo2QuotientEvaluator.sol") + { + Some(quotient_solidity) => { + let quotient_address = evm.create(compile_solidity_with_runs( + "ient_solidity, + SOLC_OPTIMIZE_RUNS, + )); + evm.create_with_two_address_args(verifier_code, vk_address, quotient_address) + } + None => evm.create_with_address_arg(verifier_code, vk_address), + }; + + // Cost of a full accepted run, used below to attribute reverts to a stage: + // an accumulator decode guard fires long before this, a binding failure + // costs almost exactly this much. + let accepted_gas; + match evm.try_call_with_gas(verifier_address, calldata.clone(), GAS_CAP) { + CallOutcome::Success { + output, gas_used, .. + } => { + let expected: Vec = [vec![0u8; 31], vec![1]].concat(); + assert_eq!( + output, expected, + "fixture proof should verify; the fixture and calldata may be out of sync" + ); + accepted_gas = gas_used; + } + CallOutcome::Revert { gas_used, output } => panic!( + "fixture proof was rejected (gas_used = {gas_used}, output = 0x{}); \ + regenerate fixtures/ivc -- see this file's header", + hex::encode(output) + ), + CallOutcome::Halt { gas_used, reason } => { + panic!("fixture proof halted (gas_used = {gas_used}, reason = {reason})") + } + } + + // Limbs are 56 bits packed 4 to a word, so the first word carries 224 + // significant bits. Byte 3 is the lowest unused high byte in the + // big-endian word: setting it keeps the value below the Fr modulus, so + // only `check_acc_coord_packing` can catch it. + let mut bad_packing = calldata.clone(); + bad_packing[first_acc_word + 3] ^= 0x01; + assert_reverts( + evm.try_call_with_gas(verifier_address, bad_packing, GAS_CAP), + "non-canonical accumulator limb packing", + ); + + // Perturb a coordinate and zero its scalar, so the term cannot be + // dismissed as a no-op multiply and has to fail the point decode. + for (case, point_word, scalar_word) in [ + ( + "malformed LHS accumulator point", + first_acc_word, + lhs_scalar_word, + ), + ( + "malformed RHS accumulator point", + rhs_first_word, + rhs_scalar_word, + ), + ] { + let mut malformed = calldata.clone(); + malformed[point_word + 31] ^= 0x01; + if has_carried_scalars { + malformed[scalar_word..scalar_word + 0x20].fill(0); + } + assert_reverts( + evm.try_call_with_gas(verifier_address, malformed, GAS_CAP), + case, + ); + } + + // Substituting the canonical point at infinity for a real accumulator term + // must not verify. This is the encoding `is_acc_encoded_identity` accepts, + // so it exercises the identity path rather than the range check. + for (case, point_word) in [ + ( + "LHS accumulator replaced with encoded infinity", + first_acc_word, + ), + ( + "RHS accumulator replaced with encoded infinity", + rhs_first_word, + ), + ] { + let mut identity = calldata.clone(); + write_encoded_identity(&mut identity, point_word, &verifier_solidity); + assert_reverts( + evm.try_call_with_gas(verifier_address, identity, GAS_CAP), + case, + ); + } + + // The canonical infinity above short-circuits at `is_acc_encoded_identity`, + // so it never enters `load_acc_point`'s coordinate-decoding branch. The two + // cases below are the non-canonical routes to the same nulled operand, and + // each is caught by a different guard inside that branch. + // + // 1. Identity flag on `x`, honest `y`. `x` decodes to zero and sets `x_is_id`, + // but `y` does not, so the malformed-infinity check (`iszero(or(or(x_hi, + // x_lo), or(y_hi, y_lo)))`) must reject. Without it, `load_acc_point` would + // still write EIP-2537 infinity into the accumulator slot while `y` was + // arbitrary. + for (case, point_word) in [ + ( + "LHS accumulator identity flag with honest y", + first_acc_word, + ), + ( + "RHS accumulator identity flag with honest y", + rhs_first_word, + ), + ] { + let mut flagged = calldata.clone(); + // Only the two `x` words; `y` keeps its honest value. + write_encoded_coordinate(&mut flagged, point_word, true, &verifier_solidity); + assert_reverts_before_transcript( + evm.try_call_with_gas(verifier_address, flagged, GAS_CAP), + accepted_gas, + case, + ); + } + + // 2. Both coordinates carry the codec's zero sentinel (`p - 1`) with no + // identity flag. Every packing and field check accepts this, so the + // `decoded_zero` guard is the only thing separating "the codec's zero" from + // "EIP-2537's point at infinity". + for (case, point_word) in [ + ( + "LHS accumulator decodes to zero without the identity flag", + first_acc_word, + ), + ( + "RHS accumulator decodes to zero without the identity flag", + rhs_first_word, + ), + ] { + let mut decoded_zero = calldata.clone(); + write_encoded_coordinate(&mut decoded_zero, point_word, false, &verifier_solidity); + write_encoded_coordinate( + &mut decoded_zero, + point_word + 2 * 0x20, + false, + &verifier_solidity, + ); + assert_reverts_before_transcript( + evm.try_call_with_gas(verifier_address, decoded_zero, GAS_CAP), + accepted_gas, + case, + ); + } + + // --------------------------------------------------------------------- + // Calldata framing. These attacks need no layout knowledge at all. + // --------------------------------------------------------------------- + let instance_count = read_u256_word(&calldata, instances_len_word); + let mut framing_cases: Vec<(String, Vec)> = Vec::new(); + + let mut trailing = calldata.clone(); + trailing.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + framing_cases.push(("extra trailing calldata".into(), trailing)); + + let mut truncated = calldata.clone(); + truncated.pop(); + framing_cases.push(("truncated calldata".into(), truncated)); + + let mut wrong_selector = calldata.clone(); + wrong_selector[3] ^= 0x01; + framing_cases.push(("wrong function selector".into(), wrong_selector)); + + for (case, offset, value) in [ + ("wrong proof ABI head", 0x04, 0x60), + ("wrong instances ABI head", 0x24, 0x20), + ( + "short proof length", + PROOF_PAYLOAD_START - 0x20, + proof_len as u64 - 0x20, + ), + ( + "long proof length", + PROOF_PAYLOAD_START - 0x20, + proof_len as u64 + 0x20, + ), + ( + "wrong instance array length", + instances_len_word, + instance_count + 1, + ), + ] { + let mut mutated = calldata.clone(); + write_u256_word(&mut mutated, offset, value); + framing_cases.push((case.into(), mutated)); + } + + for (case, mutated) in framing_cases { + assert_reverts( + evm.try_call_with_gas(verifier_address, mutated, GAS_CAP), + &case, + ); + } + + // --------------------------------------------------------------------- + // Curve-level attacks on every proof commitment. + // + // The repacked proof opens with a run of EIP-2537 padded G1 points, so the + // run length is discovered from the padding signature rather than + // hardcoded: a regenerated fixture with a different commitment count stays + // covered. + // --------------------------------------------------------------------- + let g1_count = padded_g1_block_count(&calldata, PROOF_PAYLOAD_START, proof_len); + assert!( + g1_count >= 8, + "expected a run of padded G1 commitments at the proof head, found {g1_count}; \ + the fixture proof layout changed" + ); + + let p_hi = solidity_constant(&verifier_solidity, "BLS_P_HI"); + let mut p_lo = solidity_constant(&verifier_solidity, "BLS_P_MINUS_ONE_LO"); + // p - 1 ends in ...aaaa, so incrementing cannot carry out of the low byte. + p_lo[31] += 1; + + for index in 0..g1_count { + let at = PROOF_PAYLOAD_START + index * G1_PADDED_BYTES; + + // (0, 1) is field-canonical but off the curve: y^2 = x^3 + 4 gives + // 1 != 4, so the G1 precompiles must reject it. + let mut off_curve = calldata.clone(); + off_curve[at..at + G1_PADDED_BYTES].fill(0); + off_curve[at + G1_PADDED_BYTES - 1] = 1; + + // x = p exactly: one past the largest canonical coordinate. + let mut base_modulus = calldata.clone(); + base_modulus[at..at + 0x20].copy_from_slice(&p_hi); + base_modulus[at + 0x20..at + 0x40].copy_from_slice(&p_lo); + + // EIP-2537 pads each 48-byte coordinate with 16 leading zero bytes. + // Setting one is a non-canonical encoding of an otherwise valid point. + let mut bad_padding = calldata.clone(); + bad_padding[at] ^= 0x01; + + for (label, mutated) in [ + ("off-curve", off_curve), + ("base-modulus", base_modulus), + ("non-canonical padding", bad_padding), + ] { + assert_reverts( + evm.try_call_with_gas(verifier_address, mutated, GAS_CAP), + &format!("{label} G1 at proof commitment {index}"), + ); + } + } + + // --------------------------------------------------------------------- + // Scalar canonicality across the evaluation block that follows the + // commitments, plus the non-accumulator public inputs. + // --------------------------------------------------------------------- + let fr_modulus = solidity_constant(&verifier_solidity, "FR_MODULUS"); + let evals_start = PROOF_PAYLOAD_START + g1_count * G1_PADDED_BYTES; + let evals_end = PROOF_PAYLOAD_START + proof_len; + assert!( + evals_start < evals_end && (evals_end - evals_start).is_multiple_of(0x20), + "evaluation block is not a whole number of words" + ); + + for (index, at) in (evals_start..evals_end).step_by(0x20).enumerate() { + let mut noncanonical = calldata.clone(); + noncanonical[at..at + 0x20].copy_from_slice(&fr_modulus); + assert_reverts( + evm.try_call_with_gas(verifier_address, noncanonical, GAS_CAP), + &format!("proof scalar {index} set to the Fr modulus"), + ); + } + + // The accumulator words are covered above; sweep the remaining public + // inputs for scalar canonicality. + let first_instance_word = instances_len_word + 0x20; + for index in 0..instance_count as usize { + let at = first_instance_word + index * 0x20; + if (first_acc_word..rhs_scalar_word + 0x20).contains(&at) { + continue; + } + let mut noncanonical = calldata.clone(); + noncanonical[at..at + 0x20].copy_from_slice(&fr_modulus); + assert_reverts( + evm.try_call_with_gas(verifier_address, noncanonical, GAS_CAP), + &format!("public input {index} set to the Fr modulus"), + ); + } + + // --------------------------------------------------------------------- + // Instance binding: a valid proof must not verify against different public + // inputs. + // + // Every other mutation in this file is independently caught by a range, + // packing, or framing check, so all of them would still revert if instance + // absorption regressed -- wrong order, wrong count, wrong endianness, or + // instances simply never absorbed. This case is the only one that fails + // *because* the transcript binds the instances: the mutated word stays a + // canonical field element and well inside its slot, so nothing but the + // Fiat-Shamir challenges can distinguish it. + // + // The accumulator words are excluded because they have their own decode + // guards; a revert there would not be attributable to binding. + assert!( + final_acc_offset > 0, + "fixture has no non-accumulator public input to test instance binding with" + ); + for index in 0..final_acc_offset { + let at = first_instance_word + index * 0x20; + let mut rebound = calldata.clone(); + // Flip the low bit: the nearest possible value, still canonical. + rebound[at + 31] ^= 0x01; + assert!( + rebound[at..at + 0x20] < fr_modulus[..], + "public input {index} left non-canonical by the bit flip; pick another mutation" + ); + assert_ne!( + &rebound[at..at + 0x20], + &calldata[at..at + 0x20], + "public input {index} was not actually modified" + ); + assert_reverts_at_pairing( + evm.try_call_with_gas(verifier_address, rebound, GAS_CAP), + accepted_gas, + &format!("public input {index} changed to a different canonical value"), + ); + } +} + +/// EIP-2537 padded G1: `x_hi, x_lo, y_hi, y_lo`. +const G1_PADDED_BYTES: usize = 4 * 0x20; + +/// Count the leading run of EIP-2537 padded G1 points in the repacked proof. +/// +/// Each coordinate is a 48-byte field element left-padded to 64 bytes, so the +/// high word of `x` and of `y` both start with sixteen zero bytes. Evaluation +/// scalars do not share that signature, which is what ends the run. +fn padded_g1_block_count(calldata: &[u8], proof_start: usize, proof_len: usize) -> usize { + let mut count = 0; + while (count + 1) * G1_PADDED_BYTES <= proof_len { + let at = proof_start + count * G1_PADDED_BYTES; + let x_pad_zero = calldata[at..at + 16].iter().all(|b| *b == 0); + let y_pad_zero = calldata[at + 0x40..at + 0x50].iter().all(|b| *b == 0); + if !(x_pad_zero && y_pad_zero) { + break; + } + count += 1; + } + count +} + +/// Write `value` as a big-endian EVM word at `offset`. +fn write_u256_word(calldata: &mut [u8], offset: usize, value: u64) { + calldata[offset..offset + 0x20].fill(0); + calldata[offset + 0x18..offset + 0x20].copy_from_slice(&value.to_be_bytes()); +} + +/// Read a rendered `uint256 internal constant NAME = 0x...;` out of the +/// fixture's verifier source. +/// +/// Parsing the constants back out of the artifact under test keeps this file +/// from carrying a second copy of them: if codegen ever changes the encoding, +/// the vector below follows automatically instead of silently testing a stale +/// literal. +fn solidity_constant(source: &str, name: &str) -> [u8; 0x20] { + let needle = format!("constant {name} "); + let line = source + .lines() + .map(str::trim) + .find(|line| line.starts_with("uint256") && line.contains(&needle)) + .unwrap_or_else(|| panic!("verifier source has no constant `{name}`")); + let hex_value = line + .split_once("0x") + .expect("constant should be hex") + .1 + .trim_end_matches(';') + .trim(); + let bytes = hex::decode(format!("{hex_value:0>64}")) + .unwrap_or_else(|err| panic!("constant `{name}` is not hex: {err}")); + bytes.try_into().expect("constant should be one EVM word") +} + +/// Overwrite the four accumulator coordinate words at `point_word` with the +/// canonical encoded point at infinity. +/// +/// This is the exact quadruple `is_acc_encoded_identity` accepts: `p - 1` in +/// every packed word, with the identity flag (one radix base) folded into the +/// first word of `x`. +/// Overwrite the two packed words of one accumulator coordinate at +/// `coord_word` with the codec's zero sentinel `p - 1`, optionally folding in +/// the identity flag (one radix base) as the encoder does for `x`. +/// +/// Unlike [`write_encoded_identity`] this touches a single coordinate, so the +/// result is deliberately *not* the canonical infinity quadruple and does not +/// short-circuit `is_acc_encoded_identity`. +fn write_encoded_coordinate( + calldata: &mut [u8], + coord_word: usize, + with_identity_flag: bool, + verifier_solidity: &str, +) { + let first = if with_identity_flag { + "BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG" + } else { + "BLS_P_MINUS_ONE_PACKED_0" + }; + for (index, name) in [first, "BLS_P_MINUS_ONE_PACKED_1"].into_iter().enumerate() { + let word = solidity_constant(verifier_solidity, name); + let at = coord_word + index * 0x20; + calldata[at..at + 0x20].copy_from_slice(&word); + } +} + +fn write_encoded_identity(calldata: &mut [u8], point_word: usize, verifier_solidity: &str) { + for (index, name) in [ + "BLS_P_MINUS_ONE_PACKED_0_WITH_ID_FLAG", + "BLS_P_MINUS_ONE_PACKED_1", + "BLS_P_MINUS_ONE_PACKED_0", + "BLS_P_MINUS_ONE_PACKED_1", + ] + .into_iter() + .enumerate() + { + let word = solidity_constant(verifier_solidity, name); + let at = point_word + index * 0x20; + calldata[at..at + 0x20].copy_from_slice(&word); + } +} diff --git a/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs index e28144fc0..b7a30f01f 100644 --- a/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs +++ b/proofs/solidity-verifier/tests/ivc_keccak_solidity.rs @@ -1144,6 +1144,9 @@ fn ivc_final_keccak_solidity_e2e() { trace: quotient_trace_enabled, gas_checkpoints: gas_checkpoints_enabled, }, + // Repository dumps stay provenance-free so they are byte-stable + // across commits; deployment builds set this (P10/L-8). + provenance: None, }) .expect("pinned quotient render should succeed"); let verifier_solidity = artifacts.verifier; diff --git a/proofs/solidity-verifier/tests/poseidon_fixture.rs b/proofs/solidity-verifier/tests/poseidon_fixture.rs index 58bfe73a3..36092af7f 100644 --- a/proofs/solidity-verifier/tests/poseidon_fixture.rs +++ b/proofs/solidity-verifier/tests/poseidon_fixture.rs @@ -125,13 +125,17 @@ fn poseidon_renders_compiles_and_verifies() { let srs_dir = srs_dir(); let srs_path = format!("{srs_dir}/bls_filecoin_2p{K}"); let fallback_srs_path = format!("{srs_dir}/bls_filecoin_2p19"); - if !Path::new(&srs_path).exists() && !Path::new(&fallback_srs_path).exists() { - eprintln!( - "skipping poseidon end-to-end smoke: SRS not found at {srs_path} or {fallback_srs_path}. \ - Set SRS_DIR or fetch the asset under midfall/zk_stdlib." - ); - return; - } + // The gate was explicitly requested, so a missing asset fails rather than + // silently reporting a pass that rendered and compiled nothing. + assert!( + Path::new(&srs_path).exists() || Path::new(&fallback_srs_path).exists(), + "{RUN_EVM_TESTS_ENV}=1 requires the test SRS, but it was not found at {srs_path} or \ + {fallback_srs_path}. +Fetch it with: + curl -L -o {fallback_srs_path} \ + https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19 +or point SRS_DIR at an existing copy." + ); env::set_var("SRS_DIR", &srs_dir); let relation = PoseidonExample; @@ -200,11 +204,13 @@ fn poseidon_renders_compiles_and_verifies() { verifier_solidity.len() ); - // Skip the EVM portion if the pinned solc is not available. - if !pinned_solc_available() { - eprintln!("skipping poseidon end-to-end smoke: pinned solc not available"); - return; - } + assert!( + pinned_solc_available(), + "{RUN_EVM_TESTS_ENV}=1 requires the pinned solc, which was not found or did not match. +\ + Install it, point SOLC at the binary, or set \ + HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 to accept another version." + ); let vk_creation_code = compile_solidity(&vk_solidity); let verifier_creation_code = compile_solidity(&verifier_solidity); diff --git a/proofs/solidity-verifier/tests/rsa_signature_fixture.rs b/proofs/solidity-verifier/tests/rsa_signature_fixture.rs index 132d1e4c0..f37cce3bc 100644 --- a/proofs/solidity-verifier/tests/rsa_signature_fixture.rs +++ b/proofs/solidity-verifier/tests/rsa_signature_fixture.rs @@ -103,13 +103,17 @@ fn rsa_signature_renders_compiles_and_verifies() { let srs_dir = srs_dir(); let srs_path = format!("{srs_dir}/bls_filecoin_2p{K}"); let fallback_srs_path = format!("{srs_dir}/bls_filecoin_2p19"); - if !Path::new(&srs_path).exists() && !Path::new(&fallback_srs_path).exists() { - eprintln!( - "skipping RSA signature Solidity smoke: SRS not found at {srs_path} or \ - {fallback_srs_path}. Set SRS_DIR or fetch the asset under midfall/zk_stdlib." - ); - return; - } + // The gate was explicitly requested, so a missing asset fails rather than + // silently reporting a pass that rendered and compiled nothing. + assert!( + Path::new(&srs_path).exists() || Path::new(&fallback_srs_path).exists(), + "{RUN_EVM_TESTS_ENV}=1 requires the test SRS, but it was not found at {srs_path} or \ + {fallback_srs_path}. +Fetch it with: + curl -L -o {fallback_srs_path} \ + https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19 +or point SRS_DIR at an existing copy." + ); env::set_var("SRS_DIR", &srs_dir); let relation = RsaSignatureCircuit; @@ -172,10 +176,13 @@ fn rsa_signature_renders_compiles_and_verifies() { verifier_solidity.len() ); - if !pinned_solc_available() { - eprintln!("skipping RSA signature EVM smoke: pinned solc not available"); - return; - } + assert!( + pinned_solc_available(), + "{RUN_EVM_TESTS_ENV}=1 requires the pinned solc, which was not found or did not match. +\ + Install it, point SOLC at the binary, or set \ + HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 to accept another version." + ); let vk_creation_code = compile_solidity(&vk_solidity); let verifier_creation_code = compile_solidity(&verifier_solidity); diff --git a/proofs/solidity-verifier/tests/sha_preimage_fixture.rs b/proofs/solidity-verifier/tests/sha_preimage_fixture.rs index 573038809..3ef8529f2 100644 --- a/proofs/solidity-verifier/tests/sha_preimage_fixture.rs +++ b/proofs/solidity-verifier/tests/sha_preimage_fixture.rs @@ -82,13 +82,17 @@ fn sha_preimage_renders_compiles_and_verifies() { let srs_dir = srs_dir(); let srs_path = format!("{srs_dir}/bls_filecoin_2p{K}"); let fallback_srs_path = format!("{srs_dir}/bls_filecoin_2p19"); - if !Path::new(&srs_path).exists() && !Path::new(&fallback_srs_path).exists() { - eprintln!( - "skipping SHA preimage Solidity smoke: SRS not found at {srs_path} or \ - {fallback_srs_path}. Set SRS_DIR or fetch the asset under midfall/zk_stdlib." - ); - return; - } + // The gate was explicitly requested, so a missing asset fails rather than + // silently reporting a pass that rendered and compiled nothing. + assert!( + Path::new(&srs_path).exists() || Path::new(&fallback_srs_path).exists(), + "{RUN_EVM_TESTS_ENV}=1 requires the test SRS, but it was not found at {srs_path} or \ + {fallback_srs_path}. +Fetch it with: + curl -L -o {fallback_srs_path} \ + https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_filecoin_2p19 +or point SRS_DIR at an existing copy." + ); env::set_var("SRS_DIR", &srs_dir); let relation = ShaPreimageCircuit; @@ -152,10 +156,13 @@ fn sha_preimage_renders_compiles_and_verifies() { verifier_solidity.len() ); - if !pinned_solc_available() { - eprintln!("skipping SHA preimage EVM smoke: pinned solc not available"); - return; - } + assert!( + pinned_solc_available(), + "{RUN_EVM_TESTS_ENV}=1 requires the pinned solc, which was not found or did not match. +\ + Install it, point SOLC at the binary, or set \ + HALO2_SOLIDITY_ALLOW_UNPINNED_SOLC=1 to accept another version." + ); let vk_creation_code = compile_solidity(&vk_solidity); let verifier_creation_code = compile_solidity(&verifier_solidity); diff --git a/proofs/solidity-verifier/tests/template_digest.rs b/proofs/solidity-verifier/tests/template_digest.rs new file mode 100644 index 000000000..68d11d554 --- /dev/null +++ b/proofs/solidity-verifier/tests/template_digest.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: CC0-1.0 +//! P14 (L-1, docs/audit/HALO2_VERIFIER_REVIEW_2026-08.md): the committed +//! replay fixtures compile PRE-RENDERED `.sol` sources, so a template edit +//! whose fixtures were not regenerated keeps every replay test green while +//! the deployable artifacts silently drift. This test pins a digest of the +//! whole `templates/` tree; editing any template forces an explicit +//! regeneration acknowledgement. Deliberately not feature-gated so it runs +//! in default CI with no SRS, no solc, and no EVM. + +use std::{fs, path::Path}; + +use sha3::{Digest, Keccak256}; + +/// keccak over the sorted (path, length, content) stream of `templates/`. +const EXPECTED_TEMPLATE_TREE_DIGEST: &str = + "0x07b952304f2e76417023f215b63e13226ec63c14928e4cfee3312ee8fc3913f7"; + +fn collect_files(dir: &Path, files: &mut Vec) { + for entry in fs::read_dir(dir).expect("template directory is readable") { + let path = entry.expect("template directory entry is readable").path(); + if path.is_dir() { + collect_files(&path, files); + } else { + files.push(path); + } + } +} + +#[test] +fn template_tree_digest_is_pinned() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("templates"); + let mut files = Vec::new(); + collect_files(&root, &mut files); + files.sort(); + assert!( + files.len() >= 15, + "template tree unexpectedly small ({} files); digest coverage would be vacuous", + files.len() + ); + + let mut hasher = Keccak256::new(); + for path in &files { + let rel = path + .strip_prefix(&root) + .expect("collected paths live under templates/") + .to_string_lossy() + .replace('\\', "/"); + let content = fs::read(path).expect("template file is readable"); + hasher.update((rel.len() as u64).to_be_bytes()); + hasher.update(rel.as_bytes()); + hasher.update((content.len() as u64).to_be_bytes()); + hasher.update(&content); + } + let actual = format!("0x{}", hex::encode(hasher.finalize())); + + assert_eq!( + actual, EXPECTED_TEMPLATE_TREE_DIGEST, + "\n\nThe templates/ tree changed (digest {actual}). Every committed \ + fixture and dump was rendered from the previous tree and is now \ + stale. Before updating EXPECTED_TEMPLATE_TREE_DIGEST in this test:\n\ + \x20 1. regenerate the fixture dumps (fixture tests + \ + scripts/run_ivc_bench.sh) and the committed fixtures/ where \ + applicable (see each fixtures/*/README.md);\n\ + \x20 2. update the source-commit stamps in those READMEs;\n\ + \x20 3. re-run the replay tests against the regenerated artifacts;\n\ + then pin the new digest.\n" + ); +}