diff --git a/Cargo.lock b/Cargo.lock index 41e77fc7..f0e95900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6904,6 +6904,7 @@ dependencies = [ "qp-poseidon", "qp-poseidon-core", "qp-wormhole", + "qp-wormhole-aggregator", "qp-wormhole-circuit", "qp-wormhole-circuit-builder", "qp-wormhole-prover", diff --git a/Cargo.toml b/Cargo.toml index 90eb9690..34dba4b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ qp-poseidon = { version = "1.4.0", default-features = false } qp-poseidon-core = { version = "1.4.0", default-features = false } qp-rusty-crystals-dilithium = { version = "2.4.0", default-features = false } qp-rusty-crystals-hdwallet = { version = "2.3.1" } +qp-wormhole-aggregator = { version = "2.0.1", default-features = false } qp-wormhole-circuit = { version = "2.0.1", default-features = false } qp-wormhole-circuit-builder = { version = "2.0.1", default-features = false } qp-wormhole-prover = { version = "2.0.1", default-features = false } diff --git a/docs/wormhole-zk.md b/docs/wormhole-zk.md index 343b93cd..262e9908 100644 --- a/docs/wormhole-zk.md +++ b/docs/wormhole-zk.md @@ -1,15 +1,16 @@ # Wormhole ZK: Leaf, Layer‑0, Layer‑1 -> Wormhole ZK: each leaf proof spends 1 nullifier, pays up to 2 exits; layer‑0 aggregates 16 leaves (pads with dummies); layer‑1 batches L0 proofs server‑side. Chain currently verifies only L0. +> Wormhole ZK: each leaf proof spends 1 nullifier, pays up to 2 exits; layer‑0 aggregates `N` leaves (pads with dummies); the chain accepts multiple aggregation sizes simultaneously (default: `N ∈ {2, 16}`); layer‑1 batches L0 proofs server‑side. -The wormhole flow has three proof levels. Today the runtime only verifies L0; -L1 is built in the external `qp-wormhole-aggregator` crate but is **not** wired -into `pallet-wormhole` on the current `main`. +The wormhole flow has three proof levels. The runtime verifies L0 against one +of several baked‑in verifiers, picked at call time via a `num_leaf_proofs` +argument on the extrinsic. L1 is built in the external `qp-wormhole-aggregator` +crate but is **not** wired into `pallet-wormhole` on the current `main`. | Level | Produced by | Inputs | Outputs | Verified by chain? | |------:|-------------|--------|---------|--------------------| | Leaf | Client (per transfer) | 1 nullifier (1 spend) | Up to 2 exit accounts (spend + change) | No | -| L0 | Client (aggregator) | Up to `N = 16` leaves (rest = dummies) | `2·N = 32` exit slots, `N = 16` nullifiers | **Yes** | +| L0 | Client (aggregator) | Up to `N` leaves (rest = dummies). `N` is one of `pallet_wormhole::SUPPORTED_NUM_LEAF_PROOFS`. | `2·N` exit slots, `N` nullifiers | **Yes** | | L1 | Server / delegated aggregator | `n_inner` full L0 proofs (no padding) | `n_inner · 2N` exit slots, `n_inner · N` nullifiers | **No** (not enabled) | --- @@ -44,8 +45,17 @@ Source: `qp-wormhole-circuit/src/{circuit.rs,zk_merkle_proof.rs}` and `Layer0Aggregator` in `qp-wormhole-aggregator/src/aggregator.rs` and the monolithic circuit in `src/layer0/circuit/circuit_logic.rs`. Built into the -pallet by `pallets/wormhole/build.rs`; `N = num_leaf_proofs = 16` by default -(override with the `QP_NUM_LEAF_PROOFS` env var at build time). +pallet by `pallets/wormhole/build.rs`. The pallet bakes in **one verifier per +supported `N`** simultaneously. The set of supported sizes is read from the +build environment: + + - `QP_NUM_LEAF_PROOFS_LIST=2,16` — comma‑separated list (recommended) + - `QP_NUM_LEAF_PROOFS=N` — legacy single‑value fallback + +Default if neither is set: `2,16`. `N = 2` keeps the prover under ~1 GB of +peak memory (mobile‑friendly); `N = 16` provides the strongest privacy +(largest anonymity set per submitted batch). Clients pick which one to +produce based on their device capabilities; the chain accepts both. What the L0 circuit does: @@ -76,12 +86,17 @@ be zero). ### On‑chain verification -`pallet_wormhole::verify_aggregated_proof` (`pallets/wormhole/src/lib.rs`): - -1. `validate_proof`: deserialize, parse PIs, check `asset_id == 0`, - `volume_fee_bps` matches `T::VolumeFeeRateBps::get()`, `block_hash` matches - the on‑chain header at `block_number`, no nullifier already in - `UsedNullifiers`, then run full plonky2 verification. +`pallet_wormhole::verify_aggregated_proof(origin, proof_bytes, num_leaf_proofs)` +(`pallets/wormhole/src/lib.rs`): + +1. `validate_proof`: look up the verifier for `num_leaf_proofs` (rejects with + `AggregatedVerifierNotAvailable` if no verifier was baked in for that `N`), + deserialize, parse PIs, check the parsed PI layout matches the claimed + `N` (defense‑in‑depth — the matching `common_data` already enforces this + via deserialization), check `asset_id == 0`, `volume_fee_bps` matches + `T::VolumeFeeRateBps::get()`, `block_hash` matches the on‑chain header + at `block_number`, no nullifier already in `UsedNullifiers`, then run + full plonky2 verification. 2. Mark each L0 nullifier used. 3. Walk the `2·N` exit slots, skipping any with `exit == [0;32]` or `sum == 0` (covers dummies + dedup'd slots). @@ -140,7 +155,8 @@ embeds the L0 wrapper verifier. Enabling L1 would require: | Leaf PI length (21) | `qp-wormhole-inputs/src/lib.rs` (`PUBLIC_INPUTS_FELTS_LEN`) | | L0 wrapper PI layout | `qp-wormhole-aggregator/src/layer0/circuit/constants.rs` | | L1 wrapper PI layout | `qp-wormhole-aggregator/src/layer1/circuit/constants.rs` | -| `N = num_leaf_proofs` (default 16) | `pallets/wormhole/build.rs` (`QP_NUM_LEAF_PROOFS`) | +| Supported `N` set (default `{2, 16}`) | `pallets/wormhole/build.rs` (`QP_NUM_LEAF_PROOFS_LIST`) | +| Runtime accessor for the supported set | `pallet_wormhole::SUPPORTED_NUM_LEAF_PROOFS` | | Embedded verifier bytes | `pallets/wormhole/src/lib.rs` (`AGGREGATED_VERIFIER`) | | On‑chain verify entrypoint | `pallet_wormhole::verify_aggregated_proof` | | Amount scale (10^10) | `pallets/wormhole/src/lib.rs` (`SCALE_DOWN_FACTOR`) | diff --git a/pallets/wormhole/Cargo.toml b/pallets/wormhole/Cargo.toml index e65b0586..6922034d 100644 --- a/pallets/wormhole/Cargo.toml +++ b/pallets/wormhole/Cargo.toml @@ -32,6 +32,7 @@ sp-runtime.workspace = true [build-dependencies] hex.workspace = true qp-poseidon-core.workspace = true +qp-wormhole-aggregator = { workspace = true, features = ["std"] } qp-wormhole-circuit-builder.workspace = true [dev-dependencies] diff --git a/pallets/wormhole/build.rs b/pallets/wormhole/build.rs index d58ca389..966ec6ea 100644 --- a/pallets/wormhole/build.rs +++ b/pallets/wormhole/build.rs @@ -1,24 +1,33 @@ //! Build script for pallet-wormhole. //! -//! Generates circuit binaries (aggregated_verifier.bin, aggregated_common.bin) at build time. -//! This ensures the binaries are always consistent with the circuit crate version and -//! eliminates the need to commit large binary files to the repository. +//! Generates circuit binaries (leaf + one aggregated verifier per supported +//! `num_leaf_proofs`) at build time. Binaries are embedded into the runtime +//! via `include_bytes!` in `src/lib.rs` (through the generated +//! `verifiers_manifest.rs` file in `OUT_DIR`). //! -//! Note: Circuit generation cannot be skipped for this pallet because the binaries are -//! embedded at compile time via `include_bytes!`. +//! Supported aggregation sizes are read from the environment: +//! +//! - `QP_NUM_LEAF_PROOFS_LIST=2,16` (preferred): comma-separated list. +//! - `QP_NUM_LEAF_PROOFS=N` (legacy): single value, treated as `[N]`. +//! +//! Default if neither is set: `2,16` (low-memory mobile + max-privacy desktop). + +use std::{ + env, fs, + path::{Path, PathBuf}, + time::Instant, +}; -use std::{env, path::Path, time::Instant}; +const DEFAULT_SUPPORTED: &str = "2,16"; -/// Compute Poseidon2 hash of bytes and return hex string fn poseidon_hex(data: &[u8]) -> String { let hash = qp_poseidon_core::hash_bytes(data); - hex::encode(&hash[..16]) // first 16 bytes for shorter display + hex::encode(&hash[..16]) } -/// Print hash of a generated binary file fn print_bin_hash(dir: &Path, filename: &str) { let path = dir.join(filename); - if let Ok(data) = std::fs::read(&path) { + if let Ok(data) = fs::read(&path) { println!( "cargo:warning= {}: {} bytes, hash: {}", filename, @@ -28,41 +37,114 @@ fn print_bin_hash(dir: &Path, filename: &str) { } } +fn parse_supported_list() -> Vec { + if let Ok(list) = env::var("QP_NUM_LEAF_PROOFS_LIST") { + return list + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| { + s.parse::() + .expect("QP_NUM_LEAF_PROOFS_LIST entries must be positive integers") + }) + .collect(); + } + if let Ok(single) = env::var("QP_NUM_LEAF_PROOFS") { + return vec![single + .parse() + .expect("QP_NUM_LEAF_PROOFS must be a positive integer")]; + } + DEFAULT_SUPPORTED + .split(',') + .map(|s| s.parse::().expect("default supported list parse")) + .collect() +} + +fn rename_agg_outputs(out_path: &Path, n: usize) { + for stem in ["aggregated_common", "aggregated_verifier"] { + let src = out_path.join(format!("{}.bin", stem)); + let dst = out_path.join(format!("{}_{}.bin", stem, n)); + fs::rename(&src, &dst).unwrap_or_else(|e| { + panic!("failed to rename {:?} -> {:?}: {}", src, dst, e); + }); + print_bin_hash(out_path, &format!("{}_{}.bin", stem, n)); + } +} + +fn write_manifest(out_path: &Path, supported: &[usize]) { + let mut buf = String::new(); + buf.push_str("// Auto-generated by build.rs. Do not edit.\n\n"); + buf.push_str("pub const SUPPORTED_NUM_LEAF_PROOFS: &[u32] = &["); + for (i, n) in supported.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(&n.to_string()); + } + buf.push_str("];\n\n"); + + buf.push_str( + "pub fn load_supported_verifiers(\n\ + ) -> alloc::vec::Vec<(u32, qp_wormhole_verifier::WormholeVerifier)> {\n\ + \x20 let mut out: alloc::vec::Vec<(u32, qp_wormhole_verifier::WormholeVerifier)> =\n\ + \x20 alloc::vec::Vec::new();\n", + ); + for n in supported { + buf.push_str(&format!( + " if let Ok(v) = qp_wormhole_verifier::WormholeVerifier::new_from_bytes(\n\ + \x20 include_bytes!(\"aggregated_verifier_{n}.bin\"),\n\ + \x20 include_bytes!(\"aggregated_common_{n}.bin\"),\n\ + \x20 ) {{\n\ + \x20 out.push(({n}u32, v));\n\ + \x20 }}\n", + n = n + )); + } + buf.push_str(" out\n}\n"); + + fs::write(out_path.join("verifiers_manifest.rs"), buf) + .expect("failed to write verifiers_manifest.rs"); +} + fn main() { let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); - let num_leaf_proofs: usize = env::var("QP_NUM_LEAF_PROOFS") - .unwrap_or_else(|_| "16".to_string()) - .parse() - .expect("QP_NUM_LEAF_PROOFS must be a valid usize"); + let out_path = PathBuf::from(&out_dir); + + let supported = parse_supported_list(); + assert!( + !supported.is_empty(), + "at least one supported num_leaf_proofs is required" + ); - // cargo:warning= messages are shown during build when the script runs println!( - "cargo:warning=[pallet-wormhole] Generating ZK circuit binaries (num_leaf_proofs={})...", - num_leaf_proofs + "cargo:warning=[pallet-wormhole] generating ZK circuit binaries for num_leaf_proofs={:?}...", + supported ); let start = Instant::now(); - // Generate all circuit binaries (leaf + layer-0 aggregated, no prover, no layer-1) - qp_wormhole_circuit_builder::generate_all_circuit_binaries( - Path::new(&out_dir), - false, // include_prover = false - num_leaf_proofs, - None, // num_layer0_proofs - no layer-1 aggregation - ) - .expect("Failed to generate circuit binaries"); + qp_wormhole_circuit_builder::generate_circuit_binaries(&out_path, false) + .expect("failed to generate leaf circuit binaries"); + + for n in &supported { + qp_wormhole_aggregator::layer0::circuit::build::generate_layer0_circuit_binaries( + &out_path, *n, false, + ) + .unwrap_or_else(|e| panic!("failed to build aggregation circuit for N={}: {}", n, e)); + rename_agg_outputs(&out_path, *n); + } - let elapsed = start.elapsed(); println!( "cargo:warning=[pallet-wormhole] ZK circuit binaries generated in {:.2}s", - elapsed.as_secs_f64() + start.elapsed().as_secs_f64() ); - // Print hashes of generated binaries - let out_path = Path::new(&out_dir); - print_bin_hash(out_path, "common.bin"); - print_bin_hash(out_path, "verifier.bin"); - print_bin_hash(out_path, "dummy_proof.bin"); - print_bin_hash(out_path, "aggregated_common.bin"); - print_bin_hash(out_path, "aggregated_verifier.bin"); + print_bin_hash(&out_path, "common.bin"); + print_bin_hash(&out_path, "verifier.bin"); + print_bin_hash(&out_path, "dummy_proof.bin"); + + write_manifest(&out_path, &supported); + + println!("cargo:rerun-if-env-changed=QP_NUM_LEAF_PROOFS"); + println!("cargo:rerun-if-env-changed=QP_NUM_LEAF_PROOFS_LIST"); } diff --git a/pallets/wormhole/src/benchmarking.rs b/pallets/wormhole/src/benchmarking.rs index 5fb80db1..041cc85a 100644 --- a/pallets/wormhole/src/benchmarking.rs +++ b/pallets/wormhole/src/benchmarking.rs @@ -12,6 +12,10 @@ use qp_wormhole_verifier::{ProofWithPublicInputs, C, F}; /// This proof is used to benchmark the actual deserialization and verification cost. const AGGREGATED_PROOF_HEX: &str = include_str!("../test-data/aggregated.hex"); +/// `num_leaf_proofs` the bundled benchmark proof was generated for. Worst case +/// for the verifier (largest aggregation circuit currently shipped). +const BENCH_NUM_LEAF_PROOFS: u32 = 16; + /// Maximum number of nullifiers in an aggregated proof (default aggregation size) const MAX_NULLIFIERS: u32 = 32; @@ -36,7 +40,8 @@ mod benchmarks { hex::decode(AGGREGATED_PROOF_HEX.trim()).expect("Invalid hex in test proof"); // Get verifier for deserialization - let verifier = crate::get_aggregated_verifier().expect("Aggregated verifier not available"); + let verifier = crate::get_aggregated_verifier(BENCH_NUM_LEAF_PROOFS) + .expect("Aggregated verifier not available"); // Setup: Create nullifiers in storage to simulate worst-case reads let nullifiers: Vec<[u8; 32]> = (0..MAX_NULLIFIERS) @@ -88,7 +93,8 @@ mod benchmarks { hex::decode(AGGREGATED_PROOF_HEX.trim()).expect("Invalid hex in test proof"); // Get verifier - let verifier = crate::get_aggregated_verifier().expect("Aggregated verifier not available"); + let verifier = crate::get_aggregated_verifier(BENCH_NUM_LEAF_PROOFS) + .expect("Aggregated verifier not available"); // Deserialize proof (outside the measured block since pre_validate_proof covers this) let proof = ProofWithPublicInputs::::from_bytes( diff --git a/pallets/wormhole/src/lib.rs b/pallets/wormhole/src/lib.rs index 9b4ff8fe..3dbcdf12 100644 --- a/pallets/wormhole/src/lib.rs +++ b/pallets/wormhole/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; +use alloc::collections::BTreeMap; use lazy_static::lazy_static; pub use pallet::*; pub use qp_poseidon::ToFelts; @@ -16,17 +17,25 @@ mod tests; pub mod weights; pub use weights::*; +mod verifiers_manifest { + include!(concat!(env!("OUT_DIR"), "/verifiers_manifest.rs")); +} + +pub use verifiers_manifest::SUPPORTED_NUM_LEAF_PROOFS; + lazy_static! { - static ref AGGREGATED_VERIFIER: Option = { - let verifier_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/aggregated_verifier.bin")); - let common_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/aggregated_common.bin")); - WormholeVerifier::new_from_bytes(verifier_bytes, common_bytes).ok() - }; + static ref AGGREGATED_VERIFIERS: BTreeMap = + verifiers_manifest::load_supported_verifiers().into_iter().collect(); } -/// Getter for the aggregated proof verifier -pub fn get_aggregated_verifier() -> Result<&'static WormholeVerifier, &'static str> { - AGGREGATED_VERIFIER.as_ref().ok_or("Aggregated verifier not available") +/// Look up the aggregated proof verifier for a given `num_leaf_proofs`. +/// +/// Returns `Err` if no verifier was baked in for that value at build time. +/// The set of supported sizes is exposed in [`SUPPORTED_NUM_LEAF_PROOFS`]. +pub fn get_aggregated_verifier(num_leaf_proofs: u32) -> Result<&'static WormholeVerifier, &'static str> { + AGGREGATED_VERIFIERS + .get(&num_leaf_proofs) + .ok_or("No aggregated verifier baked in for the requested num_leaf_proofs") } /// Scale factor for quantizing amounts from 12 to 2 decimal places (10^10). @@ -324,12 +333,13 @@ pub mod pallet { pub fn verify_aggregated_proof( origin: OriginFor, proof_bytes: Vec, + num_leaf_proofs: u32, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; // Full validation including ZK verification (defense-in-depth, also done in // validate_unsigned). Weight returned depends on which stage failed. - let (_proof, aggregated_inputs) = match Self::validate_proof(&proof_bytes) { + let (_proof, aggregated_inputs) = match Self::validate_proof(&proof_bytes, num_leaf_proofs) { Ok(result) => result, Err(e) => { // Determine weight based on which stage failed @@ -513,11 +523,12 @@ pub mod pallet { fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { - Call::verify_aggregated_proof { proof_bytes } => { + Call::verify_aggregated_proof { proof_bytes, num_leaf_proofs } => { // Full validation including ZK verification - prevents invalid proofs // with high amounts from entering the pool and crowding out valid txs let (_proof, inputs) = - Self::validate_proof(proof_bytes).map_err(|_| InvalidTransaction::Call)?; + Self::validate_proof(proof_bytes, *num_leaf_proofs) + .map_err(|_| InvalidTransaction::Call)?; // Priority based on total transfer volume - higher value transfers get // priority. This prevents DoS since attackers must transfer real value @@ -555,8 +566,9 @@ pub mod pallet { /// full ZK verification was attempted. fn validate_proof( proof_bytes: &[u8], + num_leaf_proofs: u32, ) -> Result<(ProofWithPublicInputs, AggregatedPublicCircuitInputs), Error> { - let verifier = crate::get_aggregated_verifier() + let verifier = crate::get_aggregated_verifier(num_leaf_proofs) .map_err(|_| Error::::AggregatedVerifierNotAvailable)?; let proof = ProofWithPublicInputs::::from_bytes( proof_bytes.to_vec(), @@ -565,6 +577,15 @@ pub mod pallet { .map_err(|_| Error::::AggregatedProofDeserializationFailed)?; let inputs = parse_aggregated_public_inputs(&proof) .map_err(|_| Error::::InvalidAggregatedPublicInputs)?; + // Defense-in-depth: the parsed PI layout must match the claimed N. This is + // already enforced by deserializing against the matching `common_data`, but + // double-checking is cheap and keeps callers honest. + let n_usize = num_leaf_proofs as usize; + ensure!( + inputs.account_data.len() == n_usize.saturating_mul(2) + && inputs.nullifiers.len() == n_usize, + Error::::InvalidAggregatedPublicInputs + ); ensure!(inputs.asset_id == 0, Error::::NonNativeAssetNotSupported); ensure!( inputs.volume_fee_bps == T::VolumeFeeRateBps::get(), diff --git a/pallets/wormhole/src/tests.rs b/pallets/wormhole/src/tests.rs index 1ea8c487..0162d5ff 100644 --- a/pallets/wormhole/src/tests.rs +++ b/pallets/wormhole/src/tests.rs @@ -289,6 +289,10 @@ mod aggregated_proof_tests { /// The D const parameter for plonky2 proofs (extension degree = 2) const D: usize = 2; + /// `num_leaf_proofs` the bundled test proof was generated for. + /// Keep this in sync with whatever produced `test-data/aggregated.hex`. + const TEST_NUM_LEAF_PROOFS: u32 = 16; + /// Real aggregated proof for testing (hex-encoded). /// Generated using: `quantus wormhole multi round` const AGGREGATED_PROOF_HEX: &str = include_str!("../test-data/aggregated.hex"); @@ -301,7 +305,8 @@ mod aggregated_proof_tests { /// Helper to deserialize the test proof fn deserialize_test_proof() -> ProofWithPublicInputs { let proof_bytes = get_test_proof_bytes(); - let verifier = crate::get_aggregated_verifier().expect("Verifier should be available"); + let verifier = crate::get_aggregated_verifier(TEST_NUM_LEAF_PROOFS) + .expect("Verifier should be available"); ProofWithPublicInputs::::from_bytes(proof_bytes, &verifier.circuit_data.common) .expect("Proof should deserialize") } @@ -342,7 +347,8 @@ mod aggregated_proof_tests { assert_noop!( Wormhole::verify_aggregated_proof( RawOrigin::Signed(account_id(1)).into(), - proof_bytes + proof_bytes, + TEST_NUM_LEAF_PROOFS, ), sp_runtime::DispatchError::BadOrigin ); @@ -355,7 +361,11 @@ mod aggregated_proof_tests { // Random invalid bytes should fail deserialization let invalid_bytes = vec![0u8; 100]; - let result = Wormhole::verify_aggregated_proof(RawOrigin::None.into(), invalid_bytes); + let result = Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + invalid_bytes, + TEST_NUM_LEAF_PROOFS, + ); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.error, Error::::AggregatedProofDeserializationFailed.into()); @@ -369,7 +379,11 @@ mod aggregated_proof_tests { // The proof references a block that doesn't exist in our mock // This should fail with BlockNotFound - let result = Wormhole::verify_aggregated_proof(RawOrigin::None.into(), proof_bytes); + let result = Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + proof_bytes, + TEST_NUM_LEAF_PROOFS, + ); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.error, Error::::BlockNotFound.into()); @@ -399,7 +413,11 @@ mod aggregated_proof_tests { let proof_bytes = get_test_proof_bytes(); - let result = Wormhole::verify_aggregated_proof(RawOrigin::None.into(), proof_bytes); + let result = Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + proof_bytes, + TEST_NUM_LEAF_PROOFS, + ); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.error, Error::::NullifierAlreadyUsed.into()); @@ -420,7 +438,11 @@ mod aggregated_proof_tests { let proof_bytes = get_test_proof_bytes(); - let result = Wormhole::verify_aggregated_proof(RawOrigin::None.into(), proof_bytes); + let result = Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + proof_bytes, + TEST_NUM_LEAF_PROOFS, + ); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.error, Error::::InvalidPublicInputs.into()); @@ -447,7 +469,11 @@ mod aggregated_proof_tests { let proof_bytes = get_test_proof_bytes(); // This should succeed - proof is valid and state matches - assert_ok!(Wormhole::verify_aggregated_proof(RawOrigin::None.into(), proof_bytes)); + assert_ok!(Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + proof_bytes, + TEST_NUM_LEAF_PROOFS, + )); // Verify nullifiers are now marked as used for nullifier in &inputs.nullifiers { @@ -503,11 +529,16 @@ mod aggregated_proof_tests { // First submission should succeed assert_ok!(Wormhole::verify_aggregated_proof( RawOrigin::None.into(), - proof_bytes.clone() + proof_bytes.clone(), + TEST_NUM_LEAF_PROOFS, )); // Second submission with same proof should fail (nullifiers already used) - let result = Wormhole::verify_aggregated_proof(RawOrigin::None.into(), proof_bytes); + let result = Wormhole::verify_aggregated_proof( + RawOrigin::None.into(), + proof_bytes, + TEST_NUM_LEAF_PROOFS, + ); assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.error, Error::::NullifierAlreadyUsed.into());