Skip to content

Halo2 circuit with vk NODE-3480#245

Open
elasticLove1 wants to merge 13 commits into
full_dex_and_bridge_test_with_final_halo2_circuitfrom
halo2_circuit_with_vk
Open

Halo2 circuit with vk NODE-3480#245
elasticLove1 wants to merge 13 commits into
full_dex_and_bridge_test_with_final_halo2_circuitfrom
halo2_circuit_with_vk

Conversation

@elasticLove1
Copy link
Copy Markdown
Member

No description provided.

aferno and others added 10 commits May 20, 2026 01:49
* 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>
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.

6 participants