Skip to content

tvm_vm(WITHVK): KZG accumulation decider + ETH light-client step/rotate fixtures - #284

Open
SergeSPb wants to merge 1 commit into
mainfrom
pruvendo/rotate-accumulator-decider
Open

SergeSPb wants to merge 1 commit into
mainfrom
pruvendo/rotate-accumulator-decider

Conversation

@SergeSPb

Copy link
Copy Markdown
Collaborator

Summary

Closes the recursive-aggregation soundness gap in ZKHALO2VERIFYWITHVK and lands the ETH beacon light-client opcode fixtures (deposit step + committee rotate, milestone M6 of acki-nacki-bridge/eth-light-client-prover).

Problem. A snark-verifier aggregation proof defers its inner KZG opening checks into a 12-limb accumulator exposed as instances[0..12]. The stock opcode runs a plain SHPLONK verify_proof (SingleStrategy, Blake2b): it proves the aggregation relation but never pairs that accumulator, so a recursive-rotate proof is accepted without checking the folded inner shard/step proofs. A BN254-only in-circuit decider is impossible (the check is a BN254 pairing), so the fix must live verifier-side — in the opcode.

Change.

  • VkBlob (zk_halo2_with_vk_bundle.rs): repurpose reserved byte 11 as accumulator_limbs (0 = none, 12 = KZG accumulator @ instances[0..12]). Backward-compatible — legacy all-zero-reserved blobs keep meaning "no decider".
  • Opcode (zk_halo2_with_vk.rs): when accumulator_limbs == 12, after the SHPLONK verify additionally run the decider e(lhs, g2) == e(rhs, s_g2) over instances[0..12] against the SRS G2 points the opcode already embeds (KZG_G2_BYTES / KZG_S_G2_BYTES, i.e. Hermez [1]G2 / [s]G2). Both must hold. The accumulator is decoded from the snark-verifier layout (lhs.x‖lhs.y‖rhs.x‖rhs.y, each 3 little-endian 88-bit limbs) via byte-window assembly, avoiding the multi-halo2 Field-trait tangle in this crate's dependency graph.
  • Fixtures: add halo2_test_data/{step,rotate}_light_client (real Hermez proofs). The rotate VkBlob header carries accumulator_limbs = 12; step is 10 PI with a 2-level committee commitment.

Native reference for the decode + pairing convention (honest PASS, tampered/​swapped REJECT): acki-nacki-bridge/eth-light-client-prover/examples/rotate_decider_check.rs.

Test plan

  • cargo test -p tvm_vm --lib decider_tests — decider on the real rotate accumulator: honest decides true; tampered limb and swapped lhs/rhs are rejected (4 tests).
  • cargo test -p tvm_vm --lib -- rotate_light_client — round-trip through the handler now runs verify and decider; honest returns true, flipped proof / tweaked accumulator limb / tweaked rotate PI return false (5 tests).
  • cargo test -p tvm_vm --lib -- step_light_client — deposit step (10 PI, 2-level commitment) round-trip + negatives (4 tests).
  • rustfmt (repo rustfmt.toml, style_edition 2024) clean on all three sources.

Made with Cursor

…te fixtures

Add a recursive-aggregation soundness gate to ZKHALO2VERIFYWITHVK and the ETH
beacon light-client opcode fixtures (deposit `step` + committee `rotate`).

Problem: a snark-verifier aggregation proof defers its inner KZG opening checks
into a 12-limb accumulator exposed as instances[0..12]. The stock opcode runs a
plain SHPLONK verify_proof (SingleStrategy, Blake2b), which proves the
aggregation relation but never pairs that accumulator — so a recursive-rotate
proof is accepted without checking the folded inner shard/step proofs.

Change:
- VkBlob: repurpose reserved byte 11 as `accumulator_limbs` (0 = none, 12 = KZG
  accumulator @ instances[0..12]). Backward-compatible: legacy all-zero-reserved
  blobs keep meaning "no decider".
- Opcode: when accumulator_limbs == 12, after the SHPLONK verify additionally run
  the decider e(lhs, g2) == e(rhs, s_g2) over instances[0..12] against the SRS
  G2 points the opcode already embeds (Hermez [1]G2 / [s]G2). Both must hold.
  The accumulator is decoded from the snark-verifier layout (lhs.x‖lhs.y‖rhs.x‖
  rhs.y, each 3 little-endian 88-bit limbs) via byte-window assembly, avoiding
  the multi-halo2 Field-trait tangle in this crate's dep graph.
- Fixtures: add halo2_test_data/{step,rotate}_light_client (real Hermez proofs
  from acki-nacki-bridge/eth-light-client-prover, M6). The rotate VkBlob header
  carries accumulator_limbs = 12.
- Tests: decider unit tests on the real rotate accumulator (honest PASS,
  tampered limb + swapped lhs/rhs REJECT) mirroring the native reference
  examples/rotate_decider_check.rs; plus round-trip step (10 PI, 2-level
  committee commitment) and rotate (15 PI) opcode tests through the handler.

Native reference for the decode+pairing convention:
acki-nacki-bridge/eth-light-client-prover/examples/rotate_decider_check.rs

Co-authored-by: Cursor <cursoragent@cursor.com>
@SergeSPb
SergeSPb force-pushed the pruvendo/rotate-accumulator-decider branch from d162e9b to 812a87b Compare September 13, 2026 12:12
@SergeSPb
SergeSPb changed the base branch from pruvendo/deposit-chainid-12pi-hermez-srs to main September 13, 2026 12:13
@SergeSPb

Copy link
Copy Markdown
Collaborator Author

Retargeted to main. The branch was rebased off pruvendo/deposit-chainid-12pi-hermez-srs onto main (d162e9b9812a87b4), so the PR is now 1 commit, 13 files, +554/−4 against mainline instead of a stacked change on a 310-commit integration branch.

This turned out to cost nothing. The two sources the decider actually touches are byte-identical on main and on the old base — zk_halo2_with_vk.rs c42cb500, zk_halo2_with_vk_bundle.rs 25dfa85d on both — so the rebase applied without conflict, including the additions to test_halo2_with_vk.rs, whose surrounding file does differ between the two branches. The embedded SRS G2 points the decider pairs against (KZG_G2_BYTES / KZG_S_G2_BYTES) are already on main; nothing about the Hermez SRS work in the old base is needed for the decider itself.

Re-verified on main (not on the old base), 13 tests:

cargo test -p tvm_vm --lib decider_tests           4 passed
cargo test -p tvm_vm --lib -- rotate_light_client  5 passed
cargo test -p tvm_vm --lib -- step_light_client    4 passed

rustfmt clean on all three sources under the repo rustfmt.toml (style_edition = "2024").

The reason for aiming at mainline rather than the integration branch: accumulator_limbs exists on no other branch in this repository — not main, pruvendo/dev, pruvendo/zkhalo2verifywithvk-on-main, halo2_shellnet, nor the old base. Until this lands, every build of tvm_vm accepts a recursive aggregation proof without pairing its deferred KZG accumulator, which is the soundness gap described above. Landing it on a pruvendo/* branch would leave that true for anything built from main.

@SergeSPb
SergeSPb requested a review from SeHor05 September 13, 2026 13:10
@SergeSPb

Copy link
Copy Markdown
Collaborator Author

@SeHor05 review request — this is the PR you flagged as unmerged in gosh-sh/bridge#36, the one that makes recursive rotate sound.

What it does. A snark-verifier aggregation proof defers its inner KZG opening checks into a 12-limb accumulator at instances[0..12]. The stock ZKHALO2VERIFYWITHVK runs only the SHPLONK verify_proof, which proves the aggregation relation but never pairs that accumulator — so a recursive rotate proof is accepted without checking the folded inner step proofs. The check is a BN254 pairing, so it cannot live in-circuit; it has to be verifier-side. This adds it: when the VkBlob header declares accumulator_limbs = 12, the opcode additionally runs the decider e(lhs, g2) == e(rhs, s_g2) against the Hermez [1]G2 / [s]G2 points it already embeds, and both checks must hold. Reserved byte 11 carries the flag, so legacy all-zero blobs keep meaning "no decider" and stay accepted as before.

What changed since you looked. It was stacked on pruvendo/deposit-chainid-12pi-hermez-srs; I rebased it onto main, so it is now 1 commit, 13 files, +554/−4 against mainline. That cost nothing — the two sources the decider touches are byte-identical on main and on the old base, and the SRS G2 points it pairs against are already there.

Evidence. 13 tests on main: 4 on the decider against the real rotate accumulator (honest decides true, tampered limb and swapped lhs/rhs rejected), 5 round-trip rotate through the handler, 4 round-trip step. All 7 CI jobs green on 812a87b4 — the run had to be started by hand (workflow_dispatch), since the auto-trigger did not fire on the force-push; nothing has run in this repo since 2026-09-08, which may be worth a look independently of this PR.

What I need. An approving review — main requires one, and my access here is push without admin, so I cannot land it myself.

One thing to be clear about so this does not read as more than it is: merging this closes the gap in mainline tvm_vm, not on shellnet. The node builds tvm_vm from a different, now-deleted branch pinned in acki-nacki/Cargo.toml, and moving that pin is a separate step I have not touched yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants