Halo2 circuit with vk NODE-3480#245
Open
elasticLove1 wants to merge 13 commits into
Open
Conversation
Fix Acki-Nacki release trigger
* Add build-actions ExecuteParams fixture * Make build_actions fixture match node wasm cache
…HPLONK verification
Adds a new gosh-feature-gated TVM opcode that verifies Halo2 SHPLONK proofs
where the verifying key, circuit configuration, public inputs, and proof
bytes are all supplied by the caller as a single self-describing operand
(`Halo2TvmBundle`). Designed for the Acki Nacki ↔ Ethereum bridge: each
deployed bridge contract carries its own immutable VK, so one opcode covers
every circuit anyone deploys to AN without growing per-circuit code in the
node.
## Wire format (frozen Q-WIRE-1..5, see docs/zkhalo2verifywithvk_design.md)
┌─────────────────────────────────────────────────────────────┐
│ "HALO2TVM" 8 B magic │
│ 0x01 1 B version │
│ 0x00 1 B transcript-kind (Blake2b SHPLONK) │
│ 0x00 × 6 6 B reserved (zero, not validated) │
│ u32 LE × 4 16 B chunk lengths │
│ config_json N B serde_json(BaseCircuitParams) │
│ vk_bytes N B VerifyingKey<G1Affine>, RawBytes │
│ instances_bytes N B strict 32-byte LE Fr × N │
│ proof_bytes N B SHPLONK proof │
└─────────────────────────────────────────────────────────────┘
The producer side lives in the sibling bridge repo at
`crates/bridge-prover-orchestrator/src/halo2_tvm_bundle.rs` and emits the
exact same byte layout.
## Implementation notes
* No new external dependencies. The handler uses public
`halo2-base = "0.5.1"` from crates.io (unchanged from origin/main) and
invokes `halo2_proofs::plonk::verify_proof::<KZGCommitmentScheme<Bn256>,
VerifierSHPLONK, Challenge255, Blake2bRead, SingleStrategy>` directly —
no `gosh-zk-snark-halo2-utils` wrapper.
* KZG SRS: only three points (`g[0]`, `g2`, `s_g2`) are embedded as
constants; `ParamsKZG<Bn256>` is reconstructed at runtime for any `k`
via `from_parts`. No on-disk SRS file is required for verification.
* Per-VK cache: bounded FIFO (8 entries) keyed by `bundle.vk_bytes`.
`config.k` is cross-checked against `vk.get_domain().k()` on both the
cold path (after VK::read) and the hot path (cache hit) as
defence-in-depth against a malicious header that lies about `k`.
* Soundness: `SerdeFormat::RawBytes` for VK read enables on-curve checks
on every G1 element. BN254 G1 is prime-order so curve membership
implies subgroup membership. Instance bytes are decoded with strict
32-byte LE `Fr::from_repr`; values ≥ modulus raise `FatalError`.
* DoS hardening: `VerifyingKey::read::<_, BaseCircuitBuilder<Fr>>` can
panic deep inside halo2-base on adversarial `BaseCircuitParams`
(e.g. `lookup_bits >= k`). The handler wraps the read in
`std::panic::catch_unwind` and converts the unwind into a structural
`FatalError`, so a single malicious bundle cannot abort the executor.
* `Halo2TvmBundle::parse` rejects: bad magic, wrong version, wrong
transcript-kind, length-prefix overrun, trailing garbage, instances
length not a multiple of 32, and bundles over 16 MiB.
## Files
* `tvm_vm/src/executor/zk_halo2_with_vk.rs` — opcode handler
* `tvm_vm/src/executor/zk_halo2_with_vk_bundle.rs` — bundle parser
* `tvm_vm/src/executor/zk_halo2_utils.rs` — embedded KZG points + test VK
* `tvm_vm/src/tests/test_halo2_with_vk.rs` — 11 integration tests
* `tvm_vm/halo2_test_data/` — DarkDex W=8 L0 fixture
* `tvm_vm/src/executor/{mod,engine/handlers,gas/gas_state}.rs` — wiring
* `tvm_assembler/src/{simple,lib}.rs` — mnemonic + round-trip guard
* `docs/zkhalo2verifywithvk_design.md` — frozen design memo
## Test status
* `cargo test -p tvm_vm --features gosh --lib` — 116 passed, 0 failed
* `cargo test -p tvm_assembler --features gosh` — 1 passed, 0 failed
* `cargo +nightly fmt --all` — clean
## Known follow-ups (out of scope for this PR)
1. Cross-version producer↔consumer integration test. Today the round-trip
fixture is generated and consumed against the same `halo2-base 0.5.1`
from crates.io. The bridge producer in the sibling repo currently
builds against the Gosh fork (`halo2-base 0.4.0` + `halo2-axiom`); a
single CI job that proves with the orchestrator and verifies through
this handler would confirm wire compatibility across versions.
2. Gas re-benchmark before mainnet. The current placeholder
`ZKHALO2_VERIFY_WITH_VK_GAS_PRICE = 5_000` is modelled on
`VERGRTH16_GAS_PRICE` scaled up for the larger VK. The cold path
includes `EvaluationDomain` construction at `k=20` which takes ~1-3 s
wall-clock — that cost is intentionally not charged at this stage.
Operators are expected to pre-warm the per-VK cache at node startup.
See `docs/zkhalo2verifywithvk_design.md` §5 / Q-GAS-1.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirrors the format of VERGRTH16 / POSEIDON entries so the new opcode shows up in the same canonical instruction reference and (via docs/SUMMARY.md) in the rendered book. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the single self-describing Halo2TvmBundle operand with three
separate stack cells matching the deploy-time/per-call lifecycle of
the data each cell carries:
top proof_cell raw SHPLONK proof bytes (no header)
↑ public_inputs_cell raw N×32 LE Fr (no header)
bottom vk_cell VkBlob (magic "VKBLOB\0\0" + version +
transcript + config_json + vk_bytes)
Rationale: vk_cell is long-lived (a TokenBridge stores it once in c4
and re-uses it on every call), public_inputs is computed per-call
from the call arguments and benefits from being headerless (O(1) to
build), proof comes verbatim from off-chain prover output.
Implementation:
* zk_halo2_with_vk.rs: handler now pops three cells, parses vk_cell
payload as VkBlob (magic VKBLOB\0\0), decodes public_inputs_cell as
strict 32-byte LE Fr × N, hands proof_cell straight to Blake2bRead.
Adds size caps: vk_blob 1 MiB, public_inputs 256 KiB, proof 1 MiB.
* zk_halo2_with_vk_bundle.rs: Halo2TvmBundle (4-chunk monolith) is
retired in favour of VkBlob (2-chunk: config_json + vk_bytes) plus
validate_public_inputs_size / validate_proof_size helpers used by
the handler before decoding the bare-bytes operands.
* Per-VK cache (bounded FIFO 8 entries) keyed by VkBlob.vk_bytes.
Defence-in-depth config.k cross-check vs vk.get_domain().k() on
both cold and hot paths. catch_unwind around VerifyingKey::read
preserved to convert adversarial-config panics in halo2-base into
structural FatalError.
* test_halo2_with_vk.rs: all 11 integration tests rewritten to push
three cells; one new test added (bridge_circuit_1b_fallback_real_
proof_verifies) consuming real fallback BLS attestation Circuit 1B
fixtures produced by the bridge-prover-orchestrator round-trip
test. Total: 12 positive + negative tests, all green.
* halo2_test_data/fallback_{vk_blob,public_inputs,proof}.bin:
checked-in real bridge Circuit 1B fixture (vk_blob 6308 B,
public_inputs 4×32 B, proof 14784 B). Generated by
EXPORT_HALO2_FIXTURE_DIR=…/halo2_test_data cargo test --test
halo2_tvm_bundle_round_trip in the bridge-prover-orchestrator.
* Documentation: zkhalo2verifywithvk_design.md and
vm-instructions/acki-nacki-vm-instructions.md rewritten to
describe the 3-operand ABI and the VkBlob byte layout. Includes
an explicit "how to assemble public_inputs_cell from contract
code" section with worked example.
Tests:
- tvm_vm --lib --tests: 118 passed (12 halo2_with_vk + 9 VkBlob
unit + 97 baseline).
- tvm_assembler gosh_zk_opcode_tests::zk_opcode_bytes_round_trip:
pass (mnemonic ZKHALO2VERIFYWITHVK → 0xC7 0x4A unchanged).
Co-authored-by: Cursor <cursoragent@cursor.com>
Pure formatting cleanup — squashes multi-line fail!() / assert!() / expression macros into single-line equivalents per current nightly rustfmt defaults. No semantic changes. CI Lint job now passes. Co-authored-by: Cursor <cursoragent@cursor.com>
The opcode's embedded KZG points and the bridge Circuit 1B fallback
test fixture now use the **Hermez Perpetual Powers of Tau** ceremony
(BN254, K=20 slice of `powersOfTau28_hez_final.ptau`) — the same
80+-contributor public trusted setup used by snarkjs, iden3 and
Polygon zkEVM. The previous test-grade deterministic SRS (which
exposed `s` via `gen_srs(K=20)`) is retired.
What changed:
- `zk_halo2_utils.rs::KZG_S_G2_BYTES` — replaced with `[s]·G2` from
the Hermez ceremony (SHA-256
`f9f0416d47fc9128e4fcac130f1ea9a0cd0c015dfe9c53a6b81476099b080979`).
`KZG_G0_BYTES` and `KZG_G2_BYTES` are unchanged — they are curve
generators (BN254 constants) shared by any well-formed SRS.
- `halo2_test_data/fallback_{vk_blob,vk,public_inputs,proof}.bin` +
`fallback_config_params.json` — regenerated end-to-end against the
Hermez SRS by `bridge-prover-orchestrator::halo2_tvm_bundle_round_trip`.
The exporter runs `verify_proof::<KZG, VerifierSHPLONK, …, Blake2bRead,
SingleStrategy>` against the fresh proof + VK + Hermez SRS before
dumping the bundle, so the checked-in fixture has passed local
verification.
- `tests/test_halo2_with_vk.rs` — migrated all 12 integration tests off
the legacy DarkDex W=8 L0 fixture (which was generated against the
test SRS and is no longer round-trippable) onto the real bridge
Circuit 1B fallback fixture. Positive tests now exercise a real
K=20 SHPLONK proof end-to-end; negative tests no longer pass
vacuously (the base case verifies, then mutations are observable
rejections).
- `halo2_test_data/dark_dex_w8_*` and `DARK_DEX_W8_VK_BYTES` —
deleted. The bridge fixture supersedes them entirely.
Test result: `cargo test --release --features gosh test_halo2_with_vk`
→ 12 passed, 0 failed (run on Hermez-based artefacts).
Cross-references:
- Producer side: `acki-nacki-bridge/scripts/bootstrap_hermez_srs.sh`
is the idempotent script that downloads `powersOfTau28_hez_final_20.ptau`
from Polygon zkEVM's public mirror, converts it to halo2 raw via
`han0110/halo2-kzg-srs` (`same_ratio` validated), and pins the
output to `params/kzg_bn254_20.srs` (SHA-256
`80394564e2598883dbb5d7d61630287f34e29cdd806d7ef74f68acc6bffeb608`).
- v3 partner pack (`halo2_tvm_for_serhii_2026-05-26`) carries this
bootstrap script + the regenerated fixture + the updated provenance
notes.
Co-authored-by: Cursor <cursoragent@cursor.com>
… halo2_circuit_with_vk
… halo2_circuit_with_vk # Conflicts: # Cargo.lock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.