From b4352d8981d6e05e0b730ac1dc7662f71c8ca245 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 24 Mar 2026 21:13:25 +0800 Subject: [PATCH 01/21] init the execution proof circuit --- arm/Cargo.toml | 1 + arm/src/execution_proof.rs | 61 +++++++ arm/src/lib.rs | 3 + arm_circuits/Cargo.lock | 18 ++ arm_circuits/Cargo.toml | 8 +- arm_circuits/execution_proof/Cargo.toml | 18 ++ .../execution_proof/methods/Cargo.toml | 10 + arm_circuits/execution_proof/methods/build.rs | 3 + .../execution_proof/methods/guest/Cargo.toml | 20 ++ .../execution_proof/methods/guest/src/main.rs | 172 ++++++++++++++++++ .../execution_proof/methods/src/lib.rs | 1 + arm_circuits/execution_proof/src/main.rs | 77 ++++++++ 12 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 arm/src/execution_proof.rs create mode 100644 arm_circuits/execution_proof/Cargo.toml create mode 100644 arm_circuits/execution_proof/methods/Cargo.toml create mode 100644 arm_circuits/execution_proof/methods/build.rs create mode 100644 arm_circuits/execution_proof/methods/guest/Cargo.toml create mode 100644 arm_circuits/execution_proof/methods/guest/src/main.rs create mode 100644 arm_circuits/execution_proof/methods/src/lib.rs create mode 100644 arm_circuits/execution_proof/src/main.rs diff --git a/arm/Cargo.toml b/arm/Cargo.toml index ef3ebe3e..a2be8d0c 100644 --- a/arm/Cargo.toml +++ b/arm/Cargo.toml @@ -42,3 +42,4 @@ bonsai = ["risc0-zkvm/bonsai"] cuda = ["risc0-zkvm/cuda"] aggregation = ["aggregation_circuit", "transaction"] aggregation_circuit = [] +execution_circuit = ["compliance_circuit", "transaction"] diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs new file mode 100644 index 00000000..51cb1621 --- /dev/null +++ b/arm/src/execution_proof.rs @@ -0,0 +1,61 @@ +//! Execution proof types for verifying transaction execution and state transitions. +//! +//! The execution proof circuit verifies a batch of transactions and produces +//! the updated commitment and nullifier tree roots. For each transaction it checks: +//! - No duplicate nullifiers within the batch +//! - The delta proof is valid +//! - The batch aggregation proof is valid +//! - Every consumed nullifier is absent from the old nullifier tree (non-inclusion), +//! proven via the nullifier sibling path leading to an empty leaf +//! +//! After verification it uses sibling paths to incrementally update both tree roots. + +use crate::{Digest, MerklePath, Transaction}; +use serde::{Deserialize, Serialize}; + +/// Public outputs of the execution proof, committed to the journal. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ExecutionProofInstance { + /// Commitment tree root before executing the transactions. + pub old_commitment_tree_root: Digest, + /// Nullifier tree root before executing the transactions. + pub old_nullifier_tree_root: Digest, + /// Commitment tree root after executing the transactions. + pub new_commitment_tree_root: Digest, + /// Nullifier tree root after executing the transactions. + pub new_nullifier_tree_root: Digest, +} + +/// Private witness for the execution proof circuit. +/// +/// Tree roots are updated incrementally using sibling paths rather than by +/// rebuilding from the full leaf set. There must be exactly one path per +/// compliance unit, ordered by transaction → action → compliance unit. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ExecutionProofWitness { + /// The transactions to execute and verify. + pub transactions: Vec, + /// Commitment tree root before executing the transactions. + /// + /// Use `Digest::default()` (all zeros) when the commitment tree is empty. + pub old_commitment_tree_root: Digest, + /// Nullifier tree root before executing the transactions. + /// + /// Use `Digest::default()` (all zeros) when the nullifier tree is empty. + pub old_nullifier_tree_root: Digest, + /// Sibling paths for commitment insertions, one per compliance unit in + /// transaction → action → compliance-unit order. + /// + /// Each path points to the empty slot where the created commitment will be + /// appended. The circuit verifies `path.root(padding_leaf()) == + /// current_commitment_root` before inserting. + pub commitment_paths: Vec, + /// Sibling paths for nullifier insertions, one per compliance unit in + /// transaction → action → compliance-unit order. + /// + /// Each path points to the empty slot where the consumed nullifier will be + /// appended. The circuit verifies `path.root(padding_leaf()) == + /// current_nullifier_root` as the non-inclusion proof, then derives the + /// new root via `path.root(nullifier)`. + pub nullifier_paths: Vec, +} diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 0079cf16..f102accc 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -14,6 +14,9 @@ pub mod constants; #[cfg(feature = "transaction")] pub mod delta_proof; pub mod error; +#[cfg(feature = "execution_circuit")] +pub mod execution_proof; +pub mod incremental_merkle_tree; pub mod logic_instance; #[cfg(feature = "transaction")] pub mod logic_proof; diff --git a/arm_circuits/Cargo.lock b/arm_circuits/Cargo.lock index 1676fd85..aa5fbe46 100644 --- a/arm_circuits/Cargo.lock +++ b/arm_circuits/Cargo.lock @@ -261,6 +261,7 @@ dependencies = [ "risc0-zkvm", "serde", "serde_with", + "sha3", "thiserror 2.0.17", ] @@ -1736,6 +1737,23 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "execution-proof" +version = "1.0.0" +dependencies = [ + "anoma-rm-risc0", + "bincode", + "execution-proof-methods", + "risc0-zkvm", +] + +[[package]] +name = "execution-proof-methods" +version = "1.0.0" +dependencies = [ + "risc0-build", +] + [[package]] name = "fallible-iterator" version = "0.3.0" diff --git a/arm_circuits/Cargo.toml b/arm_circuits/Cargo.toml index 01c08063..8d06a717 100644 --- a/arm_circuits/Cargo.toml +++ b/arm_circuits/Cargo.toml @@ -1,6 +1,12 @@ [workspace] resolver = "2" -members = ["compliance", "trivial_logic", "logic_test", "batch_aggregation"] +members = [ + "compliance", + "trivial_logic", + "logic_test", + "batch_aggregation", + "execution_proof", +] # Always optimize; otherwise tests take excessively long. [profile.dev] diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml new file mode 100644 index 00000000..1449cc21 --- /dev/null +++ b/arm_circuits/execution_proof/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "execution-proof" +version = "1.0.0" +edition = "2021" + +[dependencies] +execution-proof-methods = { path = "methods" } +risc0-zkvm = "3.0.3" +anoma-rm-risc0 = { path = "../../arm", features = [ + "execution_circuit", +], default-features = false } +bincode = "1.3.3" + +[features] +default = [] +cuda = ["risc0-zkvm/cuda"] +prove = ["risc0-zkvm/prove"] +bonsai = ["risc0-zkvm/bonsai"] diff --git a/arm_circuits/execution_proof/methods/Cargo.toml b/arm_circuits/execution_proof/methods/Cargo.toml new file mode 100644 index 00000000..c956790a --- /dev/null +++ b/arm_circuits/execution_proof/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "execution-proof-methods" +version = "1.0.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "3.0.3", features = ["unstable"] } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/arm_circuits/execution_proof/methods/build.rs b/arm_circuits/execution_proof/methods/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/arm_circuits/execution_proof/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/arm_circuits/execution_proof/methods/guest/Cargo.toml b/arm_circuits/execution_proof/methods/guest/Cargo.toml new file mode 100644 index 00000000..ed7dd4b8 --- /dev/null +++ b/arm_circuits/execution_proof/methods/guest/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "execution-proof-guest" +version = "1.0.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "3.0.3", features = [ + "std", + "unstable", +], default-features = false } +anoma-rm-risc0 = { path = "../../../../arm", features = [ + "execution_circuit", +], default-features = false } +arm-core = { package = "anoma-rm-core", path = "../../../../arm_core" } + +[patch.crates-io] +k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.1" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs new file mode 100644 index 00000000..a9f5bfef --- /dev/null +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -0,0 +1,172 @@ +use anoma_rm_risc0::{ + compliance::ComplianceInstanceWords, + constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, + delta_proof::DeltaProof, + execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, + merkle_path::{padding_leaf, MerklePathExt}, + transaction::{Delta, TransactionExt}, + utils::bytes_to_words, + Digest, +}; +use risc0_zkvm::guest::env; +use std::collections::HashSet; + +/// Converts a 32-byte VK constant to the `risc0_zkvm::sha::Digest` expected by `env::verify`. +fn vk_to_risc0(bytes: &[u8; 32]) -> risc0_zkvm::sha::Digest { + let words: [u32; 8] = arm_core::utils::bytes_to_words(bytes) + .try_into() + .expect("32 bytes always yields 8 words"); + risc0_zkvm::sha::Digest::new(words) +} + +/// Builds the aggregation circuit journal as u32 words for use with `env::verify`. +/// +/// Replicates `TransactionExt::construct_aggregation_instance` without requiring the +/// host-only `aggregation` feature flag (which pulls in the risc0-zkvm prover). +fn aggregation_instance_words( + tx: &anoma_rm_risc0::Transaction, + compliance_vk: &Digest, +) -> Vec { + let compliance_instances_u32: Vec = tx + .get_compliance_instances() + .expect("compliance instances") + .iter() + .map(|b| ComplianceInstanceWords::from_bytes(b).expect("compliance instance words")) + .collect(); + + let (lp_vks, lp_instances) = tx + .get_logic_vks_and_instances() + .expect("logic vks and instances"); + + let lp_instances_u32: Vec> = lp_instances + .iter() + .map(|b| bytes_to_words(b)) + .collect(); + + risc0_zkvm::serde::to_vec(&( + compliance_instances_u32, + compliance_vk, + lp_instances_u32, + lp_vks, + )) + .expect("serialize aggregation instance") +} + +pub fn main() { + let witness: ExecutionProofWitness = env::read(); + + // ----------------------------------------------------------------------- + // 1. Initialise running tree roots from the declared old roots. + // ----------------------------------------------------------------------- + let mut commitment_root = witness.old_commitment_tree_root; + let mut nullifier_root = witness.old_nullifier_tree_root; + let empty = padding_leaf(); + + // Index into commitment_paths / nullifier_paths consumed so far. + let mut path_idx: usize = 0; + + // ----------------------------------------------------------------------- + // 2. Cross-transaction nullifier deduplication check. + // + // No two transactions in the batch may consume the same nullifier. + // ----------------------------------------------------------------------- + let mut seen_nullifiers = HashSet::::new(); + for tx in &witness.transactions { + for action in &tx.actions { + for cu in action.get_compliance_units() { + assert!( + seen_nullifiers.insert(cu.instance.consumed_nullifier), + "duplicate nullifier across transactions" + ); + } + } + } + + // ----------------------------------------------------------------------- + // 3. Per-transaction verification and state transition. + // ----------------------------------------------------------------------- + let batch_agg_vk_risc0 = vk_to_risc0(&BATCH_AGGREGATION_VK_BYTES); + let compliance_vk_core = + Digest::try_from(COMPLIANCE_VK_BYTES.as_slice()).expect("compliance VK bytes"); + + for tx in &witness.transactions { + // --- 3a. Verify delta proof --- + let msg = tx.get_delta_msg(); + let delta_instance = tx.delta().expect("delta instance"); + match &tx.delta_proof { + Delta::Proof(core_proof) => { + let proof = + DeltaProof::from_bytes(&core_proof.0).expect("deserialize delta proof"); + DeltaProof::verify(&msg, &proof, delta_instance).expect("delta proof invalid"); + } + Delta::Witness(_) => panic!("expected delta proof, got witness"), + } + + // --- 3b. Verify the batch aggregation proof --- + // + // Every transaction submitted to the execution proof circuit must carry + // a batch aggregation proof. Individual (non-aggregated) proofs are not + // accepted; the circuit panics if the field is absent. + assert!( + tx.aggregation_proof.is_some(), + "transaction is missing an aggregation proof" + ); + let agg_words = aggregation_instance_words(tx, &compliance_vk_core); + env::verify(batch_agg_vk_risc0, &agg_words) + .expect("aggregation proof verification failed"); + + // --- 3c. Tree updates via sibling paths --- + // + // For each compliance unit: + // 1. Use the nullifier path to prove non-inclusion (the slot currently + // holds the empty/padding leaf) and derive the new nullifier root. + // 2. Use the commitment path to append the created commitment and + // derive the new commitment root. + for action in &tx.actions { + for cu in action.get_compliance_units() { + let nf = cu.instance.consumed_nullifier; + let commitment = cu.instance.created_commitment; + + let nf_path = witness + .nullifier_paths + .get(path_idx) + .expect("missing nullifier path"); + let cm_path = witness + .commitment_paths + .get(path_idx) + .expect("missing commitment path"); + + // Non-inclusion: the target slot must currently be empty. + assert_eq!( + nf_path.root(&empty), + nullifier_root, + "nullifier non-inclusion check failed: slot is not empty" + ); + + // Append nullifier: derive new nullifier root. + nullifier_root = nf_path.root(&nf); + + // Append commitment: derive new commitment root. + // The slot must also be empty before insertion. + assert_eq!( + cm_path.root(&empty), + commitment_root, + "commitment path does not lead to current commitment root" + ); + commitment_root = cm_path.root(&commitment); + + path_idx += 1; + } + } + } + + // ----------------------------------------------------------------------- + // 4. Commit the instance with the updated roots. + // ----------------------------------------------------------------------- + env::commit(&ExecutionProofInstance { + old_commitment_tree_root: witness.old_commitment_tree_root, + old_nullifier_tree_root: witness.old_nullifier_tree_root, + new_commitment_tree_root: commitment_root, + new_nullifier_tree_root: nullifier_root, + }); +} diff --git a/arm_circuits/execution_proof/methods/src/lib.rs b/arm_circuits/execution_proof/methods/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/arm_circuits/execution_proof/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs new file mode 100644 index 00000000..dbfeda80 --- /dev/null +++ b/arm_circuits/execution_proof/src/main.rs @@ -0,0 +1,77 @@ +#[cfg(feature = "prove")] +use anoma_rm_risc0::execution_proof::ExecutionProofWitness; + +pub fn main() { + // Do nothing; this is just a placeholder main function. +} + +/// Builds an ExecutorEnv with all inner receipts from the witness added as +/// assumptions, then proves the execution circuit. +#[cfg(feature = "prove")] +pub fn prove( + witness: &ExecutionProofWitness, + proof_type: risc0_zkvm::ProverOpts, +) -> Result> { + use execution_proof_methods::EXECUTION_PROOF_GUEST_ELF; + use risc0_zkvm::{default_prover, ExecutorEnv, InnerReceipt, VerifierContext}; + + let mut env_builder = ExecutorEnv::builder(); + + for tx in &witness.transactions { + if let Some(agg_bytes) = &tx.aggregation_proof { + // Add the aggregation inner receipt as an assumption. + let inner: InnerReceipt = bincode::deserialize(agg_bytes)?; + env_builder.add_assumption(inner); + } else { + // Add individual compliance and logic inner receipts as assumptions. + for inner in tx.get_compliance_inner_receipts()? { + env_builder.add_assumption(inner); + } + for inner in tx.get_logic_inner_receipts()? { + env_builder.add_assumption(inner); + } + } + } + + let env = env_builder.write(witness)?.build()?; + + let receipt = default_prover() + .prove_with_ctx( + env, + &VerifierContext::default(), + EXECUTION_PROOF_GUEST_ELF, + &proof_type, + )? + .receipt; + + Ok(receipt) +} + +// Updates the ELF binary and prints the image ID. +// Run with: cargo test --features prove print_execution_proof_elf_id -- --nocapture +#[test] +fn print_execution_proof_elf_id() { + use execution_proof_methods::{EXECUTION_PROOF_GUEST_ELF, EXECUTION_PROOF_GUEST_ID}; + + std::fs::write( + "../../arm/elfs/execution-proof-guest.bin", + EXECUTION_PROOF_GUEST_ELF, + ) + .expect("Failed to write execution proof guest ELF binary"); + + use risc0_zkvm::sha::Digest; + println!( + "EXECUTION_PROOF_GUEST_ID: {:?}", + Digest::from(EXECUTION_PROOF_GUEST_ID) + ); +} + +/// Verifies a proved execution receipt against the expected image ID. +#[cfg(feature = "prove")] +pub fn verify( + receipt: &risc0_zkvm::Receipt, +) -> Result<(), Box> { + use execution_proof_methods::EXECUTION_PROOF_GUEST_ID; + receipt.verify(EXECUTION_PROOF_GUEST_ID)?; + Ok(()) +} From 687dc8a130a601f3d115ce3a865c29118d022db3 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 24 Mar 2026 21:13:47 +0800 Subject: [PATCH 02/21] incremental merkle tree --- arm/src/incremental_merkle_tree.rs | 658 +++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 arm/src/incremental_merkle_tree.rs diff --git a/arm/src/incremental_merkle_tree.rs b/arm/src/incremental_merkle_tree.rs new file mode 100644 index 00000000..87e0d393 --- /dev/null +++ b/arm/src/incremental_merkle_tree.rs @@ -0,0 +1,658 @@ +//! Incremental Merkle tree for efficient append-only updates. +//! +//! An [`IncrementalMerkleTree`] of depth `D` stores only `D` frontier nodes +//! (called `branch`) rather than all `2^D` leaves. Each new leaf can +//! therefore be appended in O(D) time, and an authentication path is produced +//! at insertion time without rebuilding the whole tree. +//! +//! # Algorithm — binary addition +//! +//! Insertion behaves like binary addition on `count` (the number of leaves +//! inserted so far). For each level, the lowest bit of `count` tells us +//! whether the new node is a left or right child: +//! +//! ```text +//! node = new_leaf +//! index = count +//! +//! for level in 0..depth: +//! if index % 2 == 0: +//! branch[level] = node // cache the left child +//! break // nothing to merge yet +//! else: +//! node = H(branch[level], node) // merge with cached left +//! index = index / 2 +//! +//! count += 1 +//! ``` +//! +//! # Storage +//! +//! | field | description | +//! |-------|-------------| +//! | `branch[i]` | latest completed sub-tree root at level `i` | +//! | `count` | number of leaves inserted so far | +//! | `root` | cached current root, recomputed after each insert | +//! +//! Empty-subtree hashes (`ZEROS[i]`) are precomputed once at startup as a +//! module-level constant and shared across all tree instances. + +use crate::{ + error::ArmError, + merkle_path::{padding_leaf, MerklePath}, + utils::{core_to_risc0_digest, hash_two, risc0_to_core_digest}, + Digest, +}; + +/// Maximum depth an [`IncrementalMerkleTree`] may grow to. +pub const MAX_DEPTH: usize = 32; + +lazy_static::lazy_static! { + /// Precomputed empty-subtree hashes. `ZEROS[i]` is the root of a + /// fully-empty sub-tree of height `i`: + /// `ZEROS[0]` = canonical empty-leaf hash, + /// `ZEROS[i]` = `H(ZEROS[i-1], ZEROS[i-1])`. + pub static ref ZEROS: [Digest; MAX_DEPTH] = { + let mut z = [Digest::default(); MAX_DEPTH]; + z[0] = padding_leaf(); + for i in 1..MAX_DEPTH { + z[i] = hash_core(&z[i - 1], &z[i - 1]); + } + z + }; +} + +/// Hash two core [`Digest`]s together via the RISC0 SHA-256 implementation. +fn hash_core(left: &Digest, right: &Digest) -> Digest { + let l = core_to_risc0_digest(left); + let r = core_to_risc0_digest(right); + risc0_to_core_digest(hash_two(&l, &r)) +} + +/// An incremental (append-only) Merkle tree whose depth grows on demand. +/// +/// # Depth +/// +/// The initial depth is set at construction time via [`IncrementalMerkleTree::new`] +/// and increases automatically whenever [`Self::insert`] would overflow the +/// current capacity. Growth stops at [`MAX_DEPTH`]. +/// +/// # Memory +/// +/// Only `depth` frontier digests (`branch`) plus one cached root are stored, +/// regardless of how many leaves have been inserted. Empty-subtree hashes +/// are shared via the module-level [`ZEROS`] constant. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct IncrementalMerkleTree { + /// Height of the tree. + depth: usize, + /// `branch[i]` holds the latest completed sub-tree root at level `i`. + /// Updated during insertion via the binary-addition carry algorithm. + branch: Vec, + /// Cached current Merkle root; recomputed after every [`Self::insert`]. + root: Digest, + /// Number of leaves inserted so far (= index of the next leaf). + count: usize, +} + +impl IncrementalMerkleTree { + /// Creates a new, empty incremental Merkle tree with the given `depth`. + /// + /// Initialises `branch` from [`ZEROS`] and computes the root of the + /// fully-empty tree. Panics if `depth > MAX_DEPTH`. + pub fn new(depth: usize) -> Self { + let branch = ZEROS[..depth].to_vec(); + let root = Self::compute_root_inner(&branch, 0, depth); + + IncrementalMerkleTree { + depth, + branch, + root, + count: 0, + } + } + + /// Returns the depth of the tree. + pub fn depth(&self) -> usize { + self.depth + } + + /// Returns the current Merkle root. + pub fn root(&self) -> Digest { + self.root + } + + /// Returns the number of leaves that have been inserted so far. + pub fn len(&self) -> usize { + self.count + } + + /// Returns `true` if no leaves have been inserted yet. + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + /// Returns the index at which the next leaf will be inserted. + pub fn next_index(&self) -> usize { + self.count + } + + /// Maximum number of leaves this tree can hold at its current depth (`2^depth`). + /// + /// Always `Some` while `depth <= MAX_DEPTH` (32). + pub fn capacity(&self) -> Option { + 1usize.checked_shl(self.depth as u32) + } + + /// Returns the precomputed empty-subtree hashes up to the current depth. + /// + /// `zeros()[i]` is the root of a sub-tree of height `i` whose leaves are + /// all the canonical empty-leaf ([`padding_leaf()`]). + pub fn zeros(&self) -> &[Digest] { + &ZEROS[..self.depth] + } + + /// Returns a reference to the current frontier (branch) nodes. + /// + /// `branch()[i]` is the latest completed sub-tree root at level `i`. + /// Together with [`ZEROS`] this captures the entire state needed to insert + /// subsequent leaves. + pub fn branch(&self) -> &[Digest] { + &self.branch + } + + /// Increases the tree depth by 1, doubling its capacity. + /// + /// The current root becomes the completed left subtree at the new level, + /// and the right half is filled with the canonical empty subtree. + /// All previously inserted leaves and their paths remain valid. + /// + /// # Errors + /// + /// Returns [`ArmError::TreeTooLarge`] if `depth` is already at [`MAX_DEPTH`]. + pub fn grow(&mut self) -> Result<(), ArmError> { + if self.depth >= MAX_DEPTH { + return Err(ArmError::TreeTooLarge); + } + let new_depth = self.depth + 1; + + // The current full subtree becomes the completed left child at the new level. + self.branch.push(self.root); + // New root = H(old_root, empty_right) + self.root = hash_core(&self.root, &ZEROS[self.depth]); + self.depth = new_depth; + Ok(()) + } + + /// Inserts a new leaf and returns its index. + /// + /// Uses the binary-addition carry algorithm: walks from level 0 upward, + /// merging with previously cached left sub-trees whenever `count` has a 1 + /// bit at that level, and storing the result at the first 0 bit. + /// + /// If the tree is at capacity, the depth is automatically increased by 1 + /// before insertion. + /// + /// # Errors + /// + /// Returns [`ArmError::TreeTooLarge`] if the tree is already at + /// [`MAX_DEPTH`] and cannot grow further. + pub fn insert(&mut self, leaf: Digest) -> Result { + if self.count >= self.capacity().ok_or(ArmError::TreeTooLarge)? { + self.grow()?; + } + + let leaf_index = self.count; + let mut node = leaf; + let mut index = self.count; + let mut merged_all = true; + + for level in 0..self.depth { + if index % 2 == 0 { + // Left child: cache this node and stop — nothing to merge yet. + self.branch[level] = node; + merged_all = false; + break; + } else { + // Right child: merge with the cached left sub-tree and carry. + node = hash_core(&self.branch[level], &node); + index /= 2; + } + } + + self.count += 1; + + if merged_all && self.depth > 0 { + // All levels carried: `node` is the root of the now-full tree. + self.root = node; + } else { + // Recompute the cached root from the updated branch/count. + self.root = Self::compute_root_inner(&self.branch, self.count, self.depth); + } + Ok(leaf_index) + } + + /// Returns the authentication path for the leaf that will be inserted + /// *next* (at `self.next_index()`), without modifying the tree. + /// + /// The returned path is valid against the root that will exist *after* the + /// subsequent [`Self::insert`] call. Prefer [`Self::insert_and_get_path`] + /// when both operations are needed together. + /// + /// # Path encoding + /// + /// Each entry `(sibling, is_sibling_left)` describes one level: + /// * `is_sibling_left = false` — the sibling is to the right of the + /// current node (current node is a left child). + /// * `is_sibling_left = true` — the sibling is to the left (current node + /// is a right child). + /// + /// # Errors + /// + /// Returns [`ArmError::TreeTooLarge`] if the tree is already at capacity. + pub fn next_path(&self) -> Result { + let capacity = self.capacity().ok_or(ArmError::TreeTooLarge)?; + if self.count >= capacity { + return Err(ArmError::TreeTooLarge); + } + + let mut path = Vec::with_capacity(self.depth); + let mut idx = self.count; + + for level in 0..self.depth { + let entry = if idx % 2 == 0 { + // Left child: the right sibling is an empty sub-tree + (ZEROS[level], false) + } else { + // Right child: the left sibling is the last completed left sub-tree + (self.branch[level], true) + }; + path.push(entry); + idx >>= 1; + } + + Ok(MerklePath::from_path(&path)) + } + + /// Inserts a new leaf and simultaneously returns the authentication path + /// for that leaf against the updated root. + /// + /// Equivalent to calling [`Self::next_path`] followed by [`Self::insert`], + /// and guarantees that `path.root(&leaf) == self.root()` holds after the + /// call. + /// + /// # Returns + /// + /// `Ok((leaf_index, path))` where: + /// * `leaf_index` is the position of the inserted leaf. + /// * `path` is the authentication path satisfying `path.root(&leaf) == self.root()`. + /// + /// # Errors + /// + /// Returns [`ArmError::TreeTooLarge`] if the tree is already at [`MAX_DEPTH`] + /// and cannot grow further. + pub fn insert_and_get_path(&mut self, leaf: Digest) -> Result<(usize, MerklePath), ArmError> { + // Grow before computing the path so both see the same depth. + if self.count >= self.capacity().ok_or(ArmError::TreeTooLarge)? { + self.grow()?; + } + let path = self.next_path()?; + let idx = self.insert(leaf)?; + Ok((idx, path)) + } + + /// Recomputes the Merkle root from `branch` and `count`. + /// + /// Walks from level 0 to `depth-1`. At each level, if the corresponding + /// bit of `count` is set, `branch[level]` is a completed left sub-tree + /// that goes on the left; otherwise the running hash goes on the left and + /// `ZEROS[level]` fills the empty right slot. + fn compute_root_inner(branch: &[Digest], count: usize, depth: usize) -> Digest { + if depth == 0 { + return padding_leaf(); + } + + let mut current = ZEROS[0]; + for level in 0..depth { + if count & (1 << level) != 0 { + current = hash_core(&branch[level], ¤t); + } else { + current = hash_core(¤t, &ZEROS[level]); + } + } + current + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::merkle_path::MerklePathExt; + + fn small_tree() -> IncrementalMerkleTree { + IncrementalMerkleTree::new(3) + } + + // ── construction ───────────────────────────────────────────────────────── + + #[test] + fn empty_tree_has_deterministic_root() { + let t1 = small_tree(); + let t2 = small_tree(); + assert_eq!(t1.root(), t2.root()); + assert_ne!(t1.root(), Digest::default()); + } + + // ── capacity ────────────────────────────────────────────────────────────── + + #[test] + fn capacity_is_power_of_two() { + assert_eq!(IncrementalMerkleTree::new(0).capacity(), Some(1)); + assert_eq!(IncrementalMerkleTree::new(1).capacity(), Some(2)); + assert_eq!(IncrementalMerkleTree::new(3).capacity(), Some(8)); + assert_eq!(IncrementalMerkleTree::new(10).capacity(), Some(1024)); + } + + // ── bookkeeping ─────────────────────────────────────────────────────────── + + #[test] + fn len_and_is_empty() { + let mut tree = small_tree(); + assert!(tree.is_empty()); + assert_eq!(tree.len(), 0); + + let leaf = Digest::new([1, 0, 0, 0, 0, 0, 0, 0]); + tree.insert(leaf).unwrap(); + assert!(!tree.is_empty()); + assert_eq!(tree.len(), 1); + assert_eq!(tree.next_index(), 1); + } + + // ── overflow guard ──────────────────────────────────────────────────────── + + #[test] + fn next_path_at_capacity_returns_error() { + let mut tree = IncrementalMerkleTree::new(2); + let leaf = Digest::new([1, 0, 0, 0, 0, 0, 0, 0]); + for _ in 0..4 { + tree.insert(leaf).unwrap(); + } + assert!(matches!(tree.next_path(), Err(ArmError::TreeTooLarge))); + } + + // ── maximum depth ──────────────────────────────────────────────────────── + + #[test] + fn grow_at_max_depth_returns_error() { + let mut tree = IncrementalMerkleTree::new(MAX_DEPTH); + assert!(matches!(tree.grow(), Err(ArmError::TreeTooLarge))); + } + + // ── dynamic growth ──────────────────────────────────────────────────────── + + #[test] + fn insert_auto_grows_at_capacity() { + let mut tree = IncrementalMerkleTree::new(2); + let leaf = Digest::new([7, 0, 0, 0, 0, 0, 0, 0]); + // Fill the depth-2 tree (4 leaves) + for _ in 0..4 { + tree.insert(leaf).unwrap(); + } + assert_eq!(tree.depth(), 2); + assert_eq!(tree.len(), 4); + + // 5th insert should auto-grow to depth 3 + tree.insert(leaf).unwrap(); + assert_eq!(tree.depth(), 3); + assert_eq!(tree.len(), 5); + } + + #[test] + fn grown_tree_paths_verify() { + let mut tree = IncrementalMerkleTree::new(1); + // Fill depth-1 (2 leaves), then insert 2 more — triggering two grows + for i in 0..4u32 { + let leaf = Digest::new([i + 1, 0, 0, 0, 0, 0, 0, 0]); + let (_, path) = tree.insert_and_get_path(leaf).unwrap(); + assert_eq!( + path.root(&leaf), + tree.root(), + "leaf {i}: path mismatch after grow" + ); + } + assert_eq!(tree.depth(), 2); + } + + #[test] + fn explicit_grow_doubles_capacity() { + let mut tree = IncrementalMerkleTree::new(3); + assert_eq!(tree.capacity(), Some(8)); + tree.grow().unwrap(); + assert_eq!(tree.depth(), 4); + assert_eq!(tree.capacity(), Some(16)); + } + + #[test] + fn grow_on_empty_tree_preserves_root() { + let mut tree = IncrementalMerkleTree::new(2); + let root_before = tree.root(); + tree.grow().unwrap(); + // Growing an empty tree: new root = H(old_root, ZEROS[2]) + let expected = hash_core(&root_before, &ZEROS[2]); + assert_eq!(tree.root(), expected); + } + + #[test] + fn insertions_after_grow_verify() { + // Fill a depth-2 tree, let it auto-grow, and confirm all paths verify. + let mut tree = IncrementalMerkleTree::new(2); + for i in 0..8u32 { + let leaf = Digest::new([i + 1, 0, 0, 0, 0, 0, 0, 0]); + let (_, path) = tree.insert_and_get_path(leaf).unwrap(); + assert_eq!( + path.root(&leaf), + tree.root(), + "leaf {i}: path mismatch (depth {})", + tree.depth() + ); + } + // Grew once: depth 2 → 3 (capacity 4 → 8), exactly fits 8 leaves + assert_eq!(tree.depth(), 3); + assert_eq!(tree.len(), 8); + } + + // ── single insertion ────────────────────────────────────────────────────── + + #[test] + fn single_insertion_path_verifies() { + let mut tree = small_tree(); + let leaf = Digest::new([1, 2, 3, 4, 5, 6, 7, 8]); + + let (idx, path) = tree.insert_and_get_path(leaf).unwrap(); + + assert_eq!(idx, 0); + assert_eq!(path.len(), 3); + assert_eq!(path.root(&leaf), tree.root()); + } + + // ── multiple insertions ─────────────────────────────────────────────────── + + #[test] + fn all_paths_verify_at_insert_time() { + let mut tree = small_tree(); + for i in 0..8u32 { + let leaf = Digest::new([i, 0, 0, 0, 0, 0, 0, 0]); + let (_, path) = tree.insert_and_get_path(leaf).unwrap(); + assert_eq!( + path.root(&leaf), + tree.root(), + "leaf {i}: path root mismatch" + ); + } + } + + // ── next_path / insert consistency ─────────────────────────────────────── + + #[test] + fn next_path_and_insert_and_get_path_agree() { + let leaf = Digest::new([42, 0, 0, 0, 0, 0, 0, 0]); + + // Method 1: call next_path() then insert() + let mut tree1 = small_tree(); + let path1 = tree1.next_path().unwrap(); + tree1.insert(leaf).unwrap(); + + // Method 2: call insert_and_get_path() + let mut tree2 = small_tree(); + let (_, path2) = tree2.insert_and_get_path(leaf).unwrap(); + + assert_eq!(path1, path2); + assert_eq!(tree1.root(), tree2.root()); + } + + #[test] + fn next_path_verifies_after_insert() { + let mut tree = small_tree(); + for i in 0..8u32 { + let leaf = Digest::new([i, 0, 0, 0, 0, 0, 0, 0]); + let path = tree.next_path().unwrap(); + tree.insert(leaf).unwrap(); + assert_eq!(path.root(&leaf), tree.root(), "leaf {i}: path mismatch"); + } + } + + // ── root changes after each insertion ──────────────────────────────────── + + #[test] + fn root_changes_after_each_insertion() { + let mut tree = small_tree(); + let initial_root = tree.root(); + + let leaf_a = Digest::new([1, 0, 0, 0, 0, 0, 0, 0]); + tree.insert(leaf_a).unwrap(); + let root_after_first = tree.root(); + assert_ne!(root_after_first, initial_root); + + let leaf_b = Digest::new([2, 0, 0, 0, 0, 0, 0, 0]); + tree.insert(leaf_b).unwrap(); + assert_ne!(tree.root(), root_after_first); + } + + // ── distinct leaves produce distinct roots ──────────────────────────────── + + #[test] + fn distinct_leaves_produce_distinct_roots() { + let leaf_a = Digest::new([1, 0, 0, 0, 0, 0, 0, 0]); + let leaf_b = Digest::new([2, 0, 0, 0, 0, 0, 0, 0]); + + let mut tree_a = small_tree(); + tree_a.insert(leaf_a).unwrap(); + + let mut tree_b = small_tree(); + tree_b.insert(leaf_b).unwrap(); + + assert_ne!(tree_a.root(), tree_b.root()); + } + + // ── binary-addition carry ──────────────────────────────────────────────── + + #[test] + fn branch_reflects_binary_carry_pattern() { + let mut tree = IncrementalMerkleTree::new(3); + let leaves: Vec = (0..4u32) + .map(|i| Digest::new([i + 1, 0, 0, 0, 0, 0, 0, 0])) + .collect(); + + // After 1 insertion (count=1, binary 001): branch[0] = L0 + tree.insert(leaves[0]).unwrap(); + assert_eq!(tree.branch()[0], leaves[0]); + + // After 2 insertions (count=2, binary 010): branch[1] = H(L0, L1) + tree.insert(leaves[1]).unwrap(); + let h01 = hash_core(&leaves[0], &leaves[1]); + assert_eq!(tree.branch()[1], h01); + + // After 3 insertions (count=3, binary 011): branch[0] = L2 + tree.insert(leaves[2]).unwrap(); + assert_eq!(tree.branch()[0], leaves[2]); + + // After 4 insertions (count=4, binary 100): branch[2] = H(H(L0,L1), H(L2,L3)) + tree.insert(leaves[3]).unwrap(); + let h23 = hash_core(&leaves[2], &leaves[3]); + let h0123 = hash_core(&h01, &h23); + assert_eq!(tree.branch()[2], h0123); + } + + // ── full tree ──────────────────────────────────────────────────────────── + + #[test] + fn full_tree_root_is_correct() { + let mut tree = IncrementalMerkleTree::new(2); + let leaves: Vec = (0..4u32) + .map(|i| Digest::new([i + 1, 0, 0, 0, 0, 0, 0, 0])) + .collect(); + + for leaf in &leaves { + tree.insert(*leaf).unwrap(); + } + + // Manually compute the expected root + let h01 = hash_core(&leaves[0], &leaves[1]); + let h23 = hash_core(&leaves[2], &leaves[3]); + let expected_root = hash_core(&h01, &h23); + assert_eq!(tree.root(), expected_root); + } + + // ── depth-0 edge case ──────────────────────────────────────────────────── + + #[test] + fn depth_zero_tree() { + let tree = IncrementalMerkleTree::new(0); + assert_eq!(tree.capacity(), Some(1)); + assert_eq!(tree.root(), padding_leaf()); + } + + // ── path depth matches tree depth ──────────────────────────────────────── + + #[test] + fn path_length_equals_depth() { + let mut tree = small_tree(); + let leaf = Digest::new([5, 0, 0, 0, 0, 0, 0, 0]); + let (_, path) = tree.insert_and_get_path(leaf).unwrap(); + assert_eq!(path.len(), 3); + } + + // ── depth-10 tree ──────────────────────────────────────────────────────── + + #[test] + fn depth_10_tree_works() { + let mut tree = IncrementalMerkleTree::new(10); + assert_eq!(tree.capacity(), Some(1024)); + + let leaf = Digest::new([1, 2, 3, 4, 5, 6, 7, 8]); + let (idx, path) = tree.insert_and_get_path(leaf).unwrap(); + assert_eq!(idx, 0); + assert_eq!(path.len(), 10); + assert_eq!(path.root(&leaf), tree.root()); + } + + // ── consistency with existing MerkleTree ───────────────────────────────── + + #[test] + fn matches_action_tree_root() { + use crate::action_tree::MerkleTree; + + let leaves: Vec = (0..5u32) + .map(|i| Digest::new([i + 1, 0, 0, 0, 0, 0, 0, 0])) + .collect(); + + // Build the same tree with both implementations + let full_tree = MerkleTree::new(leaves.clone()); + let mut inc_tree = IncrementalMerkleTree::new(3); + for leaf in &leaves { + inc_tree.insert(*leaf).unwrap(); + } + + assert_eq!(full_tree.root().unwrap(), inc_tree.root()); + } +} From 6265dd67d45b2c17d0be749bf9d9992223a227b1 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 24 Mar 2026 21:39:53 +0800 Subject: [PATCH 03/21] integrate the incremental merkle tree into the execution proof circuit --- arm/src/compliance_unit.rs | 3 +- arm/src/execution_proof.rs | 37 ++++++++++--------- .../execution_proof/methods/guest/src/main.rs | 34 +++++++---------- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/arm/src/compliance_unit.rs b/arm/src/compliance_unit.rs index 80382052..7e9786e2 100644 --- a/arm/src/compliance_unit.rs +++ b/arm/src/compliance_unit.rs @@ -6,7 +6,7 @@ use k256::ProjectivePoint; use risc0_zkvm::InnerReceipt; use crate::{ - compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt, ComplianceWitness}, + compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt}, constants::COMPLIANCE_VK, error::ArmError, }; @@ -15,6 +15,7 @@ use crate::{ use crate::{ constants::COMPLIANCE_PK, proving_system::{prove, ProofType}, + compliance::ComplianceWitness, }; /// Extension methods for compliance units that require zkvm/k256 functionality. diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 51cb1621..d4e486ac 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -8,48 +8,49 @@ //! - Every consumed nullifier is absent from the old nullifier tree (non-inclusion), //! proven via the nullifier sibling path leading to an empty leaf //! -//! After verification it uses sibling paths to incrementally update both tree roots. +//! Commitment insertions are handled by an [`IncrementalMerkleTree`] carried in +//! the witness; nullifier insertions still use explicit sibling paths so that +//! non-inclusion can be checked before each insertion. -use crate::{Digest, MerklePath, Transaction}; +use crate::{incremental_merkle_tree::IncrementalMerkleTree, Digest, MerklePath, Transaction}; use serde::{Deserialize, Serialize}; /// Public outputs of the execution proof, committed to the journal. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ExecutionProofInstance { /// Commitment tree root before executing the transactions. pub old_commitment_tree_root: Digest, /// Nullifier tree root before executing the transactions. pub old_nullifier_tree_root: Digest, - /// Commitment tree root after executing the transactions. - pub new_commitment_tree_root: Digest, + /// Commitment tree state after executing the transactions. + /// + /// `new_commitment_tree.root()` gives the updated commitment root. + pub new_commitment_tree: IncrementalMerkleTree, /// Nullifier tree root after executing the transactions. pub new_nullifier_tree_root: Digest, } /// Private witness for the execution proof circuit. /// -/// Tree roots are updated incrementally using sibling paths rather than by -/// rebuilding from the full leaf set. There must be exactly one path per -/// compliance unit, ordered by transaction → action → compliance unit. +/// Commitment updates are driven by an [`IncrementalMerkleTree`] whose state +/// is advanced by calling `insert` for each created commitment. Nullifier +/// updates still use explicit sibling paths so that non-inclusion can be +/// checked before each insertion. There must be exactly one nullifier path +/// per compliance unit, ordered by transaction → action → compliance unit. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { /// The transactions to execute and verify. pub transactions: Vec, - /// Commitment tree root before executing the transactions. + /// Commitment tree state before executing the transactions. /// - /// Use `Digest::default()` (all zeros) when the commitment tree is empty. - pub old_commitment_tree_root: Digest, + /// The circuit reads `commitment_tree.root()` as the old commitment root, + /// then calls `commitment_tree.insert(commitment)` for each created + /// commitment. Use `IncrementalMerkleTree::new(0)` for an empty tree. + pub commitment_tree: IncrementalMerkleTree, /// Nullifier tree root before executing the transactions. /// /// Use `Digest::default()` (all zeros) when the nullifier tree is empty. pub old_nullifier_tree_root: Digest, - /// Sibling paths for commitment insertions, one per compliance unit in - /// transaction → action → compliance-unit order. - /// - /// Each path points to the empty slot where the created commitment will be - /// appended. The circuit verifies `path.root(padding_leaf()) == - /// current_commitment_root` before inserting. - pub commitment_paths: Vec, /// Sibling paths for nullifier insertions, one per compliance unit in /// transaction → action → compliance-unit order. /// diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index a9f5bfef..cd737126 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -56,13 +56,14 @@ pub fn main() { let witness: ExecutionProofWitness = env::read(); // ----------------------------------------------------------------------- - // 1. Initialise running tree roots from the declared old roots. + // 1. Initialise running tree state from the witness. // ----------------------------------------------------------------------- - let mut commitment_root = witness.old_commitment_tree_root; + let mut commitment_tree = witness.commitment_tree; + let old_commitment_tree_root = commitment_tree.root(); let mut nullifier_root = witness.old_nullifier_tree_root; let empty = padding_leaf(); - // Index into commitment_paths / nullifier_paths consumed so far. + // Index into nullifier_paths consumed so far. let mut path_idx: usize = 0; // ----------------------------------------------------------------------- @@ -115,13 +116,12 @@ pub fn main() { env::verify(batch_agg_vk_risc0, &agg_words) .expect("aggregation proof verification failed"); - // --- 3c. Tree updates via sibling paths --- + // --- 3c. Tree updates --- // // For each compliance unit: // 1. Use the nullifier path to prove non-inclusion (the slot currently // holds the empty/padding leaf) and derive the new nullifier root. - // 2. Use the commitment path to append the created commitment and - // derive the new commitment root. + // 2. Insert the created commitment into the incremental tree. for action in &tx.actions { for cu in action.get_compliance_units() { let nf = cu.instance.consumed_nullifier; @@ -131,10 +131,6 @@ pub fn main() { .nullifier_paths .get(path_idx) .expect("missing nullifier path"); - let cm_path = witness - .commitment_paths - .get(path_idx) - .expect("missing commitment path"); // Non-inclusion: the target slot must currently be empty. assert_eq!( @@ -146,14 +142,10 @@ pub fn main() { // Append nullifier: derive new nullifier root. nullifier_root = nf_path.root(&nf); - // Append commitment: derive new commitment root. - // The slot must also be empty before insertion. - assert_eq!( - cm_path.root(&empty), - commitment_root, - "commitment path does not lead to current commitment root" - ); - commitment_root = cm_path.root(&commitment); + // Append commitment into the incremental tree. + commitment_tree + .insert(commitment) + .expect("commitment tree insert failed"); path_idx += 1; } @@ -161,12 +153,12 @@ pub fn main() { } // ----------------------------------------------------------------------- - // 4. Commit the instance with the updated roots. + // 4. Commit the instance with the updated state. // ----------------------------------------------------------------------- env::commit(&ExecutionProofInstance { - old_commitment_tree_root: witness.old_commitment_tree_root, + old_commitment_tree_root, old_nullifier_tree_root: witness.old_nullifier_tree_root, - new_commitment_tree_root: commitment_root, + new_commitment_tree: commitment_tree, new_nullifier_tree_root: nullifier_root, }); } From 7661fe82255fccf1fdfd93f422cd2a4413d3c197 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Wed, 25 Mar 2026 16:19:43 +0800 Subject: [PATCH 04/21] indexed merkle tree --- arm/src/indexed_merkle_tree.rs | 588 +++++++++++++++++++++++ arm/src/lib.rs | 1 + arm_circuits/execution_proof/src/main.rs | 4 +- 3 files changed, 590 insertions(+), 3 deletions(-) create mode 100644 arm/src/indexed_merkle_tree.rs diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs new file mode 100644 index 00000000..c7981423 --- /dev/null +++ b/arm/src/indexed_merkle_tree.rs @@ -0,0 +1,588 @@ +//! Indexed Merkle tree for efficient nullifier non-membership proofs and insertion. +//! +//! An [`IndexedMerkleTree`] stores values in a **sorted linked list** committed +//! by a Merkle tree. Every leaf records `(value, next_value)`, forming a chain +//! of intervals that together cover the entire value space from [`MIN_VALUE`] to +//! [`MAX_VALUE`]. +//! +//! # Non-membership proof +//! +//! To prove `v ∉ S`, show the *predecessor* leaf `(lo → hi)` such that +//! `lo < v < hi`. Since the chain is sorted and gapless, `v` cannot exist +//! elsewhere in the set. The proof is: +//! +//! 1. A Merkle path proving `(lo → hi)` is in the tree. +//! 2. Range check: `lo < v < hi`. +//! +//! # Insertion +//! +//! Inserting `v` with predecessor `(lo → hi)`: +//! +//! 1. Update the predecessor leaf to `(lo → v)`. +//! 2. Append a new leaf `(v → hi)`. +//! +//! Only **one path update** and **one append** are needed regardless of tree size. +//! +//! # Storage +//! +//! | field | description | +//! |-------|-------------| +//! | `leaves` | physical leaf array in insertion order | +//! | `depth` | current Merkle tree depth (auto-grows) | + +use crate::{ + error::ArmError, + incremental_merkle_tree::ZEROS, + merkle_path::{padding_leaf, MerklePath, MerklePathExt}, + utils::{core_to_risc0_digest, hash_two, risc0_to_core_digest}, + Digest, +}; +use serde::{Deserialize, Serialize}; + +lazy_static::lazy_static! { + /// Minimum sentinel value (lower bound; always the first leaf). + /// + /// Equal to the all-zeros digest. Real nullifiers are SHA-256 outputs and + /// will never equal this value. + pub static ref MIN_VALUE: Digest = Digest::new([0u32; 8]); + + /// Maximum sentinel value (`∞`; upper bound of the last leaf's range). + /// + /// Equal to the all-`0xFFFFFFFF` digest. Real nullifiers are SHA-256 outputs + /// and will never equal this value. + pub static ref MAX_VALUE: Digest = Digest::new([u32::MAX; 8]); +} + +// ── Internal helpers ────────────────────────────────────────────────────────── + +fn hash_pair(left: &Digest, right: &Digest) -> Digest { + risc0_to_core_digest(hash_two( + &core_to_risc0_digest(left), + &core_to_risc0_digest(right), + )) +} + +/// Returns `true` if `a < b`, treating the `[u32; 8]` words as a big-endian +/// integer (word 0 is most significant). +fn digest_lt(a: &Digest, b: &Digest) -> bool { + for (&wa, &wb) in a.as_words().iter().zip(b.as_words().iter()) { + match wa.cmp(&wb) { + std::cmp::Ordering::Less => return true, + std::cmp::Ordering::Greater => return false, + std::cmp::Ordering::Equal => {} + } + } + false // equal +} + +// ── Public types ────────────────────────────────────────────────────────────── + +/// A leaf in the indexed Merkle tree. +/// +/// Each leaf records its own value and a pointer to the next-larger value in +/// the set, forming a sorted linked list across all leaves. The leaf hash is +/// `H(value, next_value)`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct IndexedLeaf { + /// This leaf's value. + pub value: Digest, + /// The next-larger value in the set, or [`MAX_VALUE`] if this is the + /// largest. + pub next_value: Digest, +} + +impl IndexedLeaf { + /// Computes the leaf commitment hash: `H(value, next_value)`. + pub fn hash(&self) -> Digest { + hash_pair(&self.value, &self.next_value) + } +} + +/// Proof that a value is **not** in the indexed Merkle tree. +/// +/// Identifies the predecessor leaf `(lo → hi)` with `lo < target < hi` and +/// provides its Merkle authentication path against the tree root. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NonMembershipProof { + /// Predecessor leaf: `value < target < next_value`. + pub predecessor: IndexedLeaf, + /// Merkle path authenticating `predecessor` against the tree root. + pub path: MerklePath, +} + +impl NonMembershipProof { + /// Verifies that `target` is not in the tree with the given `root`. + /// + /// Returns an error if the proof is invalid. + pub fn verify(&self, target: &Digest, root: &Digest) -> Result<(), ArmError> { + if !digest_lt(&self.predecessor.value, target) { + return Err(ArmError::ProofVerificationFailed( + "non-membership proof invalid: target ≤ predecessor value".into(), + )); + } + if !digest_lt(target, &self.predecessor.next_value) { + return Err(ArmError::ProofVerificationFailed( + "non-membership proof invalid: target ≥ predecessor next_value".into(), + )); + } + if self.path.root(&self.predecessor.hash()) != *root { + return Err(ArmError::ProofVerificationFailed( + "non-membership proof invalid: path does not match root".into(), + )); + } + Ok(()) + } +} + +/// Witness for inserting a single value into the indexed Merkle tree. +/// +/// Inserting `v` with predecessor `(lo → hi)` performs two tree operations: +/// +/// 1. Update predecessor from `(lo → hi)` to `(lo → v)` — path update. +/// 2. Append new leaf `(v → hi)` — incremental insert. +/// +/// When the tree's depth increases to accommodate the new leaf, `grew = true` +/// and `predecessor_path` is computed at the new (larger) depth. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InsertionWitness { + /// Predecessor leaf before insertion. + pub predecessor: IndexedLeaf, + /// Merkle path for the predecessor leaf, valid against the *insertion root*. + /// + /// The insertion root equals `old_root` when `grew = false`, or + /// `H(old_root, ZEROS[d - 1])` when `grew = true` (where + /// `d = predecessor_path.len()`). + pub predecessor_path: MerklePath, + /// Merkle path for the new leaf `(v → hi)`, valid against the *intermediate + /// root* (after updating the predecessor). + pub new_leaf_path: MerklePath, + /// `true` if the tree depth increased by 1 to fit the new leaf. + pub grew: bool, +} + +impl InsertionWitness { + /// Verifies non-membership of `value`, applies the insertion, and returns + /// the new Merkle root. + /// + /// Returns an error if the witness is inconsistent with `old_root`. + pub fn apply(&self, value: &Digest, old_root: &Digest) -> Result { + // --- Non-membership check --- + if !digest_lt(&self.predecessor.value, value) { + return Err(ArmError::ProofVerificationFailed( + "insertion invalid: value ≤ predecessor value".into(), + )); + } + if !digest_lt(value, &self.predecessor.next_value) { + return Err(ArmError::NullifierDuplication); + } + + // --- Compute root at insertion depth (account for possible growth) --- + // + // When the tree grew, the insertion root is H(old_root, ZEROS[old_depth]) + // where old_depth = predecessor_path.len() - 1 (the depth *before* growth). + let insertion_root = if self.grew { + let old_depth = self + .predecessor_path + .len() + .checked_sub(1) + .ok_or(ArmError::InvalidLeaf)?; + hash_pair(old_root, &ZEROS[old_depth]) + } else { + *old_root + }; + + // --- Verify predecessor exists at the insertion root --- + if self.predecessor_path.root(&self.predecessor.hash()) != insertion_root { + return Err(ArmError::ProofVerificationFailed( + "insertion invalid: predecessor path does not match root".into(), + )); + } + + // --- Compute intermediate root after updating the predecessor --- + let updated_pred = IndexedLeaf { + value: self.predecessor.value, + next_value: *value, + }; + let intermediate_root = self.predecessor_path.root(&updated_pred.hash()); + + // --- Verify the new leaf slot is currently empty --- + if self.new_leaf_path.root(&padding_leaf()) != intermediate_root { + return Err(ArmError::ProofVerificationFailed( + "insertion invalid: new leaf slot is not empty".into(), + )); + } + + // --- Append new leaf and return the new root --- + let new_leaf = IndexedLeaf { + value: *value, + next_value: self.predecessor.next_value, + }; + Ok(self.new_leaf_path.root(&new_leaf.hash())) + } +} + +// ── Host-side helpers ───────────────────────────────────────────────────────── + +/// Builds a complete Merkle tree of the given `depth` from `leaf_hashes`. +/// +/// Positions `>= leaf_hashes.len()` are padded with [`padding_leaf()`]. +/// Returns `layers` where `layers[0]` is the leaf level and +/// `layers[depth]` is `[root]`. +fn build_layers(leaf_hashes: &[Digest], depth: usize) -> Vec> { + let capacity = 1usize << depth; + let mut level: Vec = (0..capacity) + .map(|i| leaf_hashes.get(i).copied().unwrap_or_else(padding_leaf)) + .collect(); + let mut layers = vec![level.clone()]; + while level.len() > 1 { + level = level + .chunks(2) + .map(|pair| hash_pair(&pair[0], &pair[1])) + .collect(); + layers.push(level.clone()); + } + layers +} + +/// Extracts a [`MerklePath`] for leaf `index` from a prebuilt layer array. +fn extract_path(layers: &[Vec], index: usize) -> MerklePath { + let depth = layers.len().saturating_sub(1); + let mut path = Vec::with_capacity(depth); + let mut idx = index; + for level in &layers[..depth] { + let is_right_child = idx % 2 == 1; + let sibling_idx = if is_right_child { idx - 1 } else { idx + 1 }; + // second element: true = sibling is to the left (current is right child) + path.push((level[sibling_idx], is_right_child)); + idx /= 2; + } + MerklePath::from_path(&path) +} + +// ── Host-side tree ──────────────────────────────────────────────────────────── + +/// Host-side indexed Merkle tree: generates [`NonMembershipProof`]s and +/// [`InsertionWitness`]es. +/// +/// Leaves are stored in **insertion order** (not sorted); the sorted linked +/// list is maintained through each leaf's `next_value` pointer. Path +/// generation rebuilds the full Merkle tree at the current depth, which is +/// O(2^depth) but runs only on the host. +/// +/// A secondary `sorted_index` — a `Vec<(Digest, usize)>` kept sorted by +/// `Digest` value — enables O(log n) predecessor lookup via binary search. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexedMerkleTree { + /// Physical leaf array (insertion order). + leaves: Vec, + /// Current Merkle tree depth. + depth: usize, + /// Sorted index: `(leaf.value, physical_index_in_leaves)`, ordered by + /// value using the same big-endian word comparison as [`digest_lt`]. + sorted_index: Vec<(Digest, usize)>, +} + +impl IndexedMerkleTree { + /// Creates a new empty indexed Merkle tree. + /// + /// Inserts the initial sentinel leaf `(MIN_VALUE → MAX_VALUE)` so that + /// every real value has a valid predecessor. + pub fn new() -> Self { + Self { + leaves: vec![IndexedLeaf { + value: *MIN_VALUE, + next_value: *MAX_VALUE, + }], + depth: 0, + sorted_index: vec![(*MIN_VALUE, 0)], + } + } + + /// Returns the current Merkle root. + pub fn root(&self) -> Digest { + let hashes: Vec = self.leaves.iter().map(|l| l.hash()).collect(); + // build_layers always returns at least one layer with at least one element. + build_layers(&hashes, self.depth)[self.depth][0] + } + + /// Returns the number of leaves (including the [`MIN_VALUE`] sentinel). + pub fn len(&self) -> usize { + self.leaves.len() + } + + /// Returns `true` if the tree contains only the sentinel leaf. + pub fn is_empty(&self) -> bool { + self.leaves.len() == 1 + } + + /// Generates a [`NonMembershipProof`] for `value`. + /// + /// Returns an error if `value` is already in the set or equals [`MIN_VALUE`]. + pub fn prove_non_membership(&self, value: &Digest) -> Result { + let pred_idx = self.predecessor_index(value)?; + let predecessor = self.leaves[pred_idx].clone(); + if !digest_lt(value, &predecessor.next_value) { + return Err(ArmError::NullifierDuplication); + } + let hashes: Vec = self.leaves.iter().map(|l| l.hash()).collect(); + let layers = build_layers(&hashes, self.depth); + Ok(NonMembershipProof { + predecessor, + path: extract_path(&layers, pred_idx), + }) + } + + /// Inserts `value` and returns the [`InsertionWitness`]. + /// + /// Returns an error if `value` is already in the set, equals [`MIN_VALUE`], + /// or equals [`MAX_VALUE`]. + pub fn insert(&mut self, value: Digest) -> Result { + if !digest_lt(&MIN_VALUE, &value) { + return Err(ArmError::InvalidLeaf); + } + if !digest_lt(&value, &MAX_VALUE) { + return Err(ArmError::InvalidLeaf); + } + + let pred_idx = self.predecessor_index(&value)?; + let predecessor = self.leaves[pred_idx].clone(); + if !digest_lt(&value, &predecessor.next_value) { + return Err(ArmError::NullifierDuplication); + } + + let n = self.leaves.len(); + let grew = n + 1 > (1 << self.depth); + let new_depth = if grew { self.depth + 1 } else { self.depth }; + + // predecessor_path at new_depth against the insertion root + let current_hashes: Vec = self.leaves.iter().map(|l| l.hash()).collect(); + let tree_before = build_layers(¤t_hashes, new_depth); + let predecessor_path = extract_path(&tree_before, pred_idx); + + // rebuild with updated predecessor, then get new_leaf_path + let mut updated_hashes = current_hashes; + updated_hashes[pred_idx] = IndexedLeaf { + value: predecessor.value, + next_value: value, + } + .hash(); + let tree_after_update = build_layers(&updated_hashes, new_depth); + let new_leaf_path = extract_path(&tree_after_update, n); + + // apply mutation + self.leaves[pred_idx].next_value = value; + self.leaves.push(IndexedLeaf { + value, + next_value: predecessor.next_value, + }); + self.depth = new_depth; + let ins = self.sorted_index.partition_point(|(v, _)| digest_lt(v, &value)); + self.sorted_index.insert(ins, (value, n)); + + Ok(InsertionWitness { + predecessor, + predecessor_path, + new_leaf_path, + grew, + }) + } + + /// Returns the physical index of the predecessor leaf for `value`. + /// + /// Uses binary search on `sorted_index` (O(log n)): finds the last entry + /// whose value is strictly less than `value`. + fn predecessor_index(&self, value: &Digest) -> Result { + // partition_point returns the first position where the predicate is + // false, i.e. the first entry with value >= target. The predecessor + // is the entry immediately before it. + let pos = self.sorted_index.partition_point(|(v, _)| digest_lt(v, value)); + if pos == 0 { + return Err(ArmError::InvalidLeaf); + } + Ok(self.sorted_index[pos - 1].1) + } +} + +impl Default for IndexedMerkleTree { + fn default() -> Self { + Self::new() + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn d(v: u32) -> Digest { + Digest::new([v, 0, 0, 0, 0, 0, 0, 0]) + } + + // ── digest_lt ──────────────────────────────────────────────────────────── + + #[test] + fn digest_lt_basic() { + assert!(digest_lt(&d(1), &d(2))); + assert!(!digest_lt(&d(2), &d(1))); + assert!(!digest_lt(&d(1), &d(1))); + } + + #[test] + fn digest_lt_sentinels() { + assert!(digest_lt(&*MIN_VALUE, &d(1))); + assert!(digest_lt(&d(1), &*MAX_VALUE)); + assert!(!digest_lt(&*MAX_VALUE, &d(1))); + } + + // ── IndexedLeaf::hash ──────────────────────────────────────────────────── + + #[test] + fn leaf_hash_is_deterministic() { + let l = IndexedLeaf { value: d(10), next_value: d(20) }; + assert_eq!(l.hash(), l.hash()); + assert_ne!(l.hash(), IndexedLeaf { value: d(10), next_value: d(30) }.hash()); + } + + // ── non-membership proof ───────────────────────────────────────────────── + + #[test] + fn non_membership_on_empty_tree() { + let tree = IndexedMerkleTree::new(); + let target = d(42); + let proof = tree.prove_non_membership(&target).unwrap(); + proof.verify(&target, &tree.root()).unwrap(); + } + + #[test] + fn non_membership_after_insertions() { + let mut tree = IndexedMerkleTree::new(); + tree.insert(d(10)).unwrap(); + tree.insert(d(30)).unwrap(); + + // 20 is between 10 and 30 → not in set + let target = d(20); + let proof = tree.prove_non_membership(&target).unwrap(); + proof.verify(&target, &tree.root()).unwrap(); + } + + // ── insertion witness ──────────────────────────────────────────────────── + + #[test] + fn first_insertion_updates_root() { + let mut tree = IndexedMerkleTree::new(); + let old_root = tree.root(); + let witness = tree.insert(d(42)).unwrap(); + let new_root = tree.root(); + + assert_ne!(old_root, new_root); + assert_eq!(witness.apply(&d(42), &old_root).unwrap(), new_root); + } + + #[test] + fn multiple_insertions_all_witnesses_valid() { + let values = [d(10), d(50), d(30), d(20), d(40)]; + let mut tree = IndexedMerkleTree::new(); + + for &v in &values { + let old_root = tree.root(); + let witness = tree.insert(v).unwrap(); + let new_root = tree.root(); + assert_eq!( + witness.apply(&v, &old_root).unwrap(), + new_root, + "witness invalid for value {:?}", + v + ); + } + } + + #[test] + fn non_membership_then_insertion_consistent() { + let mut tree = IndexedMerkleTree::new(); + tree.insert(d(10)).unwrap(); + tree.insert(d(30)).unwrap(); + + let target = d(20); + let nm_proof = tree.prove_non_membership(&target).unwrap(); + nm_proof.verify(&target, &tree.root()).unwrap(); + + // inserting the same value changes the root + let old_root = tree.root(); + let witness = tree.insert(target).unwrap(); + let new_root = tree.root(); + assert_eq!(witness.apply(&target, &old_root).unwrap(), new_root); + + // 20 is now in the set; non-membership for 25 should still work + let proof2 = tree.prove_non_membership(&d(25)).unwrap(); + proof2.verify(&d(25), &tree.root()).unwrap(); + } + + #[test] + fn insertion_triggers_tree_growth() { + let mut tree = IndexedMerkleTree::new(); + // depth=0 → capacity=1 (full with sentinel); first insert must grow + let witness = tree.insert(d(1)).unwrap(); + assert!(witness.grew); + assert_eq!(tree.depth, 1); + + // second insert stays at depth=1 (capacity=2, now has 3 leaves after sentinel) + // actually depth=1 → capacity=2; with sentinel + 1 we have 2, full again + let old_root = tree.root(); + let witness2 = tree.insert(d(2)).unwrap(); + assert!(witness2.grew); // 2+1=3 > 2^1=2 → must grow to depth 2 + assert_eq!(witness2.apply(&d(2), &old_root).unwrap(), tree.root()); + } + + // ── build_layers / extract_path consistency ─────────────────────────────── + + #[test] + fn build_layers_root_matches_direct_hash() { + let leaves = vec![d(1), d(2), d(3), d(4)]; + let layers = build_layers(&leaves, 2); + let expected = hash_pair( + &hash_pair(&d(1), &d(2)), + &hash_pair(&d(3), &d(4)), + ); + assert_eq!(*layers.last().unwrap().first().unwrap(), expected); + } + + #[test] + fn extract_path_round_trips() { + let leaves = vec![d(1), d(2), d(3), d(4)]; + let layers = build_layers(&leaves, 2); + let root = *layers.last().unwrap().first().unwrap(); + for (i, &leaf) in leaves.iter().enumerate() { + let path = extract_path(&layers, i); + assert_eq!(path.root(&leaf), root, "path mismatch at index {i}"); + } + } + + // ── error cases ─────────────────────────────────────────────────────────── + + #[test] + fn insert_min_value_returns_error() { + let mut tree = IndexedMerkleTree::new(); + assert!(tree.insert(*MIN_VALUE).is_err()); + } + + #[test] + fn insert_max_value_returns_error() { + let mut tree = IndexedMerkleTree::new(); + assert!(tree.insert(*MAX_VALUE).is_err()); + } + + #[test] + fn insert_duplicate_returns_error() { + let mut tree = IndexedMerkleTree::new(); + tree.insert(d(42)).unwrap(); + assert!(tree.insert(d(42)).is_err()); + } + + #[test] + fn non_membership_of_existing_value_returns_error() { + let mut tree = IndexedMerkleTree::new(); + tree.insert(d(42)).unwrap(); + assert!(tree.prove_non_membership(&d(42)).is_err()); + } +} diff --git a/arm/src/lib.rs b/arm/src/lib.rs index f102accc..3bfd9723 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -17,6 +17,7 @@ pub mod error; #[cfg(feature = "execution_circuit")] pub mod execution_proof; pub mod incremental_merkle_tree; +pub mod indexed_merkle_tree; pub mod logic_instance; #[cfg(feature = "transaction")] pub mod logic_proof; diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index dbfeda80..68f10192 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -68,9 +68,7 @@ fn print_execution_proof_elf_id() { /// Verifies a proved execution receipt against the expected image ID. #[cfg(feature = "prove")] -pub fn verify( - receipt: &risc0_zkvm::Receipt, -) -> Result<(), Box> { +pub fn verify(receipt: &risc0_zkvm::Receipt) -> Result<(), Box> { use execution_proof_methods::EXECUTION_PROOF_GUEST_ID; receipt.verify(EXECUTION_PROOF_GUEST_ID)?; Ok(()) From 46fbe23f61310e622629b4c8e32c2404e12b0aa4 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Wed, 25 Mar 2026 19:23:18 +0800 Subject: [PATCH 05/21] integrate the indexed merkle tree into the execution proof --- arm/src/compliance_unit.rs | 2 +- arm/src/execution_proof.rs | 83 ++++++++++--------- arm/src/indexed_merkle_tree.rs | 27 ++++-- .../execution_proof/methods/guest/src/main.rs | 78 +++++++++-------- 4 files changed, 108 insertions(+), 82 deletions(-) diff --git a/arm/src/compliance_unit.rs b/arm/src/compliance_unit.rs index 7e9786e2..bd0b5b5b 100644 --- a/arm/src/compliance_unit.rs +++ b/arm/src/compliance_unit.rs @@ -13,9 +13,9 @@ use crate::{ #[cfg(feature = "prove")] use crate::{ + compliance::ComplianceWitness, constants::COMPLIANCE_PK, proving_system::{prove, ProofType}, - compliance::ComplianceWitness, }; /// Extension methods for compliance units that require zkvm/k256 functionality. diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index d4e486ac..7f5a2029 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -1,62 +1,71 @@ -//! Execution proof types for verifying transaction execution and state transitions. +//! Execution proof types for verifying a batch of transactions against shared +//! commitment and nullifier state. //! -//! The execution proof circuit verifies a batch of transactions and produces -//! the updated commitment and nullifier tree roots. For each transaction it checks: -//! - No duplicate nullifiers within the batch -//! - The delta proof is valid -//! - The batch aggregation proof is valid -//! - Every consumed nullifier is absent from the old nullifier tree (non-inclusion), -//! proven via the nullifier sibling path leading to an empty leaf +//! The circuit takes an [`ExecutionProofWitness`] and produces an +//! [`ExecutionProofInstance`] committed to the RISC0 journal. For each +//! transaction it checks: //! -//! Commitment insertions are handled by an [`IncrementalMerkleTree`] carried in -//! the witness; nullifier insertions still use explicit sibling paths so that -//! non-inclusion can be checked before each insertion. +//! 1. **Nullifier uniqueness** — no two compliance units in the batch consume +//! the same nullifier. It also implies that all commimtments are unique. +//! 2. **Delta proof** — the transaction's delta proof is valid. +//! 3. **Aggregation proof** — all compliance and logic proofs within the +//! transaction have been aggregated and verified. +//! 4. **Nullifier non-membership + insertion** — each consumed nullifier is +//! absent from the indexed nullifier tree ([`InsertionWitness::apply`] +//! proves non-membership and returns the updated root atomically). +//! 5. **Commitment insertion** — each created commitment is appended to the +//! incremental commitment tree. -use crate::{incremental_merkle_tree::IncrementalMerkleTree, Digest, MerklePath, Transaction}; +use crate::{ + incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, Digest, + Transaction, +}; use serde::{Deserialize, Serialize}; /// Public outputs of the execution proof, committed to the journal. +/// +/// Downstream verifiers chain proofs by checking that +/// `old_commitment_tree_root` and `old_nullifier_tree_root` of a later proof +/// match the outputs of the preceding one. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ExecutionProofInstance { - /// Commitment tree root before executing the transactions. + /// Commitment tree root before the batch was executed. pub old_commitment_tree_root: Digest, - /// Nullifier tree root before executing the transactions. + /// Nullifier tree root before the batch was executed. pub old_nullifier_tree_root: Digest, - /// Commitment tree state after executing the transactions. + /// Full commitment tree state after executing the batch. /// + /// Carries the incremental tree so the next proof can continue inserting + /// commitments without re-proving the prior state. /// `new_commitment_tree.root()` gives the updated commitment root. pub new_commitment_tree: IncrementalMerkleTree, - /// Nullifier tree root after executing the transactions. + /// Nullifier tree root after executing the batch. pub new_nullifier_tree_root: Digest, } -/// Private witness for the execution proof circuit. +/// Private witness consumed by the execution proof circuit. +/// +/// Commitment updates are driven by [`commitment_tree`], whose state is +/// advanced by `insert` for each created commitment. Nullifier updates are +/// driven by [`nullifier_witnesses`], one per compliance unit — each witness +/// simultaneously proves non-membership of the consumed nullifier and derives +/// the new nullifier root via [`InsertionWitness::apply`]. /// -/// Commitment updates are driven by an [`IncrementalMerkleTree`] whose state -/// is advanced by calling `insert` for each created commitment. Nullifier -/// updates still use explicit sibling paths so that non-inclusion can be -/// checked before each insertion. There must be exactly one nullifier path -/// per compliance unit, ordered by transaction → action → compliance unit. +/// [`commitment_tree`]: ExecutionProofWitness::commitment_tree +/// [`nullifier_witnesses`]: ExecutionProofWitness::nullifier_witnesses #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { /// The transactions to execute and verify. pub transactions: Vec, - /// Commitment tree state before executing the transactions. - /// - /// The circuit reads `commitment_tree.root()` as the old commitment root, - /// then calls `commitment_tree.insert(commitment)` for each created - /// commitment. Use `IncrementalMerkleTree::new(0)` for an empty tree. + /// Incremental commitment tree state before the batch. pub commitment_tree: IncrementalMerkleTree, - /// Nullifier tree root before executing the transactions. - /// - /// Use `Digest::default()` (all zeros) when the nullifier tree is empty. + /// Indexed nullifier tree root before the batch. pub old_nullifier_tree_root: Digest, - /// Sibling paths for nullifier insertions, one per compliance unit in - /// transaction → action → compliance-unit order. + /// Nullifier insertion witnesses in transaction → action → compliance-unit + /// order; one entry per compliance unit across the entire batch. /// - /// Each path points to the empty slot where the consumed nullifier will be - /// appended. The circuit verifies `path.root(padding_leaf()) == - /// current_nullifier_root` as the non-inclusion proof, then derives the - /// new root via `path.root(nullifier)`. - pub nullifier_paths: Vec, + /// Each [`InsertionWitness`] proves that the consumed nullifier is absent + /// from the current nullifier tree root and returns the root after + /// insertion, threading state forward to the next witness. + pub nullifier_witnesses: Vec, } diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs index c7981423..f41c5637 100644 --- a/arm/src/indexed_merkle_tree.rs +++ b/arm/src/indexed_merkle_tree.rs @@ -376,7 +376,9 @@ impl IndexedMerkleTree { next_value: predecessor.next_value, }); self.depth = new_depth; - let ins = self.sorted_index.partition_point(|(v, _)| digest_lt(v, &value)); + let ins = self + .sorted_index + .partition_point(|(v, _)| digest_lt(v, &value)); self.sorted_index.insert(ins, (value, n)); Ok(InsertionWitness { @@ -395,7 +397,9 @@ impl IndexedMerkleTree { // partition_point returns the first position where the predicate is // false, i.e. the first entry with value >= target. The predecessor // is the entry immediately before it. - let pos = self.sorted_index.partition_point(|(v, _)| digest_lt(v, value)); + let pos = self + .sorted_index + .partition_point(|(v, _)| digest_lt(v, value)); if pos == 0 { return Err(ArmError::InvalidLeaf); } @@ -439,9 +443,19 @@ mod tests { #[test] fn leaf_hash_is_deterministic() { - let l = IndexedLeaf { value: d(10), next_value: d(20) }; + let l = IndexedLeaf { + value: d(10), + next_value: d(20), + }; assert_eq!(l.hash(), l.hash()); - assert_ne!(l.hash(), IndexedLeaf { value: d(10), next_value: d(30) }.hash()); + assert_ne!( + l.hash(), + IndexedLeaf { + value: d(10), + next_value: d(30) + } + .hash() + ); } // ── non-membership proof ───────────────────────────────────────────────── @@ -540,10 +554,7 @@ mod tests { fn build_layers_root_matches_direct_hash() { let leaves = vec![d(1), d(2), d(3), d(4)]; let layers = build_layers(&leaves, 2); - let expected = hash_pair( - &hash_pair(&d(1), &d(2)), - &hash_pair(&d(3), &d(4)), - ); + let expected = hash_pair(&hash_pair(&d(1), &d(2)), &hash_pair(&d(3), &d(4))); assert_eq!(*layers.last().unwrap().first().unwrap(), expected); } diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index cd737126..8e58711f 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -3,7 +3,6 @@ use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, delta_proof::DeltaProof, execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, - merkle_path::{padding_leaf, MerklePathExt}, transaction::{Delta, TransactionExt}, utils::bytes_to_words, Digest, @@ -19,10 +18,11 @@ fn vk_to_risc0(bytes: &[u8; 32]) -> risc0_zkvm::sha::Digest { risc0_zkvm::sha::Digest::new(words) } -/// Builds the aggregation circuit journal as u32 words for use with `env::verify`. +/// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. /// -/// Replicates `TransactionExt::construct_aggregation_instance` without requiring the -/// host-only `aggregation` feature flag (which pulls in the risc0-zkvm prover). +/// Replicates `TransactionExt::construct_aggregation_instance` without +/// requiring the host-only `aggregation` feature flag (which pulls in the +/// RISC0 prover stack). fn aggregation_instance_words( tx: &anoma_rm_risc0::Transaction, compliance_vk: &Digest, @@ -56,20 +56,26 @@ pub fn main() { let witness: ExecutionProofWitness = env::read(); // ----------------------------------------------------------------------- - // 1. Initialise running tree state from the witness. + // 1. Initialise running state. + // + // Both tree roots are derived from the witness rather than taken as + // explicit inputs, so they are bound to the witness data. // ----------------------------------------------------------------------- let mut commitment_tree = witness.commitment_tree; let old_commitment_tree_root = commitment_tree.root(); let mut nullifier_root = witness.old_nullifier_tree_root; - let empty = padding_leaf(); - // Index into nullifier_paths consumed so far. - let mut path_idx: usize = 0; + // Flat cursor into `witness.nullifier_witnesses`, advanced once per + // compliance unit across all transactions and actions. + let mut nullifier_witness_idx: usize = 0; // ----------------------------------------------------------------------- - // 2. Cross-transaction nullifier deduplication check. + // 2. Batch-wide nullifier uniqueness check. // - // No two transactions in the batch may consume the same nullifier. + // The indexed nullifier tree already prevents re-spending a nullifier + // that was inserted in a prior batch. This check additionally prevents + // two compliance units *within this batch* from consuming the same + // nullifier before any of them reach the tree-update step. // ----------------------------------------------------------------------- let mut seen_nullifiers = HashSet::::new(); for tx in &witness.transactions { @@ -91,7 +97,10 @@ pub fn main() { Digest::try_from(COMPLIANCE_VK_BYTES.as_slice()).expect("compliance VK bytes"); for tx in &witness.transactions { - // --- 3a. Verify delta proof --- + // --- 3a. Delta proof --- + // + // Verifies that the net value change (Σ created − Σ consumed) across + // all compliance units in the transaction is zero. let msg = tx.get_delta_msg(); let delta_instance = tx.delta().expect("delta instance"); match &tx.delta_proof { @@ -103,11 +112,11 @@ pub fn main() { Delta::Witness(_) => panic!("expected delta proof, got witness"), } - // --- 3b. Verify the batch aggregation proof --- + // --- 3b. Batch aggregation proof --- // - // Every transaction submitted to the execution proof circuit must carry - // a batch aggregation proof. Individual (non-aggregated) proofs are not - // accepted; the circuit panics if the field is absent. + // Confirms that every compliance proof and logic proof inside this + // transaction has been verified by the batch aggregation circuit. + // Transactions without an aggregation proof are rejected. assert!( tx.aggregation_proof.is_some(), "transaction is missing an aggregation proof" @@ -116,44 +125,41 @@ pub fn main() { env::verify(batch_agg_vk_risc0, &agg_words) .expect("aggregation proof verification failed"); - // --- 3c. Tree updates --- + // --- 3c. Per-compliance-unit tree updates --- + // + // For each compliance unit, in order: // - // For each compliance unit: - // 1. Use the nullifier path to prove non-inclusion (the slot currently - // holds the empty/padding leaf) and derive the new nullifier root. - // 2. Insert the created commitment into the incremental tree. + // Nullifier: `InsertionWitness::apply` proves that `consumed_nullifier` + // is not yet in the indexed nullifier tree (non-membership), + // inserts it, and returns the new nullifier root. + // + // Commitment: `IncrementalMerkleTree::insert` appends `created_commitment` + // to the incremental commitment tree. for action in &tx.actions { for cu in action.get_compliance_units() { let nf = cu.instance.consumed_nullifier; let commitment = cu.instance.created_commitment; - let nf_path = witness - .nullifier_paths - .get(path_idx) - .expect("missing nullifier path"); - - // Non-inclusion: the target slot must currently be empty. - assert_eq!( - nf_path.root(&empty), - nullifier_root, - "nullifier non-inclusion check failed: slot is not empty" - ); + let nf_witness = witness + .nullifier_witnesses + .get(nullifier_witness_idx) + .expect("missing nullifier insertion witness"); - // Append nullifier: derive new nullifier root. - nullifier_root = nf_path.root(&nf); + nullifier_root = nf_witness + .apply(&nf, &nullifier_root) + .expect("nullifier insertion witness invalid"); - // Append commitment into the incremental tree. commitment_tree .insert(commitment) .expect("commitment tree insert failed"); - path_idx += 1; + nullifier_witness_idx += 1; } } } // ----------------------------------------------------------------------- - // 4. Commit the instance with the updated state. + // 4. Commit the instance. // ----------------------------------------------------------------------- env::commit(&ExecutionProofInstance { old_commitment_tree_root, From 77e294f32d9ba7ae527822390ee5f88064541a49 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Wed, 25 Mar 2026 19:41:24 +0800 Subject: [PATCH 06/21] improve the indexed merkle tree --- arm/src/indexed_merkle_tree.rs | 299 ++++++++++++++++++++------------- 1 file changed, 178 insertions(+), 121 deletions(-) diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs index f41c5637..63eec364 100644 --- a/arm/src/indexed_merkle_tree.rs +++ b/arm/src/indexed_merkle_tree.rs @@ -16,19 +16,25 @@ //! //! # Insertion //! -//! Inserting `v` with predecessor `(lo → hi)`: +//! Inserting `v` with predecessor `(lo → hi)` requires exactly two tree +//! operations regardless of tree size: //! -//! 1. Update the predecessor leaf to `(lo → v)`. -//! 2. Append a new leaf `(v → hi)`. +//! 1. **Predecessor update**: rewrite the predecessor leaf from `(lo → hi)` +//! to `(lo → v)` via a Merkle path update. +//! 2. **New leaf append**: insert a new leaf `(v → hi)` at the next free slot. //! -//! Only **one path update** and **one append** are needed regardless of tree size. +//! # Host vs circuit split //! -//! # Storage +//! [`IndexedMerkleTree`] is a **host-side** data structure. It holds the full +//! leaf array and a [`BTreeMap`] secondary index for O(log n) predecessor +//! lookup, and it generates [`NonMembershipProof`]s and [`InsertionWitness`]es +//! that the circuit verifies cheaply without touching the full tree. //! -//! | field | description | -//! |-------|-------------| -//! | `leaves` | physical leaf array in insertion order | -//! | `depth` | current Merkle tree depth (auto-grows) | +//! | struct | where it runs | purpose | +//! |--------|---------------|---------| +//! | [`IndexedMerkleTree`] | host only | generates witnesses | +//! | [`NonMembershipProof`] | host + circuit | proves `v ∉ S` | +//! | [`InsertionWitness`] | host + circuit | proves non-membership and updates root | use crate::{ error::ArmError, @@ -38,23 +44,25 @@ use crate::{ Digest, }; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; lazy_static::lazy_static! { /// Minimum sentinel value (lower bound; always the first leaf). /// /// Equal to the all-zeros digest. Real nullifiers are SHA-256 outputs and - /// will never equal this value. + /// will never collide with this value. pub static ref MIN_VALUE: Digest = Digest::new([0u32; 8]); /// Maximum sentinel value (`∞`; upper bound of the last leaf's range). /// - /// Equal to the all-`0xFFFFFFFF` digest. Real nullifiers are SHA-256 outputs - /// and will never equal this value. + /// Equal to the all-`0xFFFFFFFF` digest. Real nullifiers are SHA-256 + /// outputs and will never collide with this value. pub static ref MAX_VALUE: Digest = Digest::new([u32::MAX; 8]); } // ── Internal helpers ────────────────────────────────────────────────────────── +/// Hashes two [`Digest`]s together via the RISC0 SHA-256 implementation. fn hash_pair(left: &Digest, right: &Digest) -> Digest { risc0_to_core_digest(hash_two( &core_to_risc0_digest(left), @@ -62,17 +70,34 @@ fn hash_pair(left: &Digest, right: &Digest) -> Digest { )) } -/// Returns `true` if `a < b`, treating the `[u32; 8]` words as a big-endian -/// integer (word 0 is most significant). +/// Returns `true` if `a < b` under a big-endian word-by-word comparison +/// (word 0 most significant). +/// +/// This is identical to the lexicographic order that `[u32]::cmp` already +/// provides, so the implementation delegates directly to slice comparison. fn digest_lt(a: &Digest, b: &Digest) -> bool { - for (&wa, &wb) in a.as_words().iter().zip(b.as_words().iter()) { - match wa.cmp(&wb) { - std::cmp::Ordering::Less => return true, - std::cmp::Ordering::Greater => return false, - std::cmp::Ordering::Equal => {} - } + a.as_words() < b.as_words() +} + +/// Newtype wrapper around [`Digest`] that exposes a total order consistent +/// with [`digest_lt`] (big-endian word-by-word comparison). +/// +/// Used as the key type in [`IndexedMerkleTree::sorted_index`] so that a +/// [`BTreeMap`] can answer predecessor queries with `range(..key).next_back()` +/// in O(log n). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct DigestKey(Digest); + +impl PartialOrd for DigestKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for DigestKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.as_words().cmp(other.0.as_words()) } - false // equal } // ── Public types ────────────────────────────────────────────────────────────── @@ -100,11 +125,13 @@ impl IndexedLeaf { /// Proof that a value is **not** in the indexed Merkle tree. /// -/// Identifies the predecessor leaf `(lo → hi)` with `lo < target < hi` and -/// provides its Merkle authentication path against the tree root. +/// The proof identifies the predecessor leaf `(lo → hi)` satisfying +/// `lo < target < hi` and provides its Merkle authentication path. Because +/// the linked list is sorted and covers the entire value space, the existence +/// of such an interval is sufficient to conclude `target ∉ S`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NonMembershipProof { - /// Predecessor leaf: `value < target < next_value`. + /// Predecessor leaf satisfying `predecessor.value < target < predecessor.next_value`. pub predecessor: IndexedLeaf, /// Merkle path authenticating `predecessor` against the tree root. pub path: MerklePath, @@ -113,7 +140,10 @@ pub struct NonMembershipProof { impl NonMembershipProof { /// Verifies that `target` is not in the tree with the given `root`. /// - /// Returns an error if the proof is invalid. + /// Checks the interval bound `predecessor.value < target < predecessor.next_value` + /// and that the predecessor is authenticated by `path` against `root`. + /// + /// Returns an error if any check fails. pub fn verify(&self, target: &Digest, root: &Digest) -> Result<(), ArmError> { if !digest_lt(&self.predecessor.value, target) { return Err(ArmError::ProofVerificationFailed( @@ -136,37 +166,57 @@ impl NonMembershipProof { /// Witness for inserting a single value into the indexed Merkle tree. /// -/// Inserting `v` with predecessor `(lo → hi)` performs two tree operations: +/// Inserting `v` with predecessor `(lo → hi)` performs two operations: /// -/// 1. Update predecessor from `(lo → hi)` to `(lo → v)` — path update. -/// 2. Append new leaf `(v → hi)` — incremental insert. +/// 1. **Predecessor update**: rewrite `(lo → hi)` to `(lo → v)` in-place via +/// `predecessor_path`. +/// 2. **New leaf append**: insert `(v → hi)` at the next free slot via +/// `new_leaf_path`. /// -/// When the tree's depth increases to accommodate the new leaf, `grew = true` -/// and `predecessor_path` is computed at the new (larger) depth. +/// The two paths are computed at the *post-growth* depth: if the tree had to +/// grow to fit the new leaf, `grew = true` and both paths have length +/// `new_depth = old_depth + 1`. [`apply`] accounts for this by first +/// reconstructing the grown root before checking `predecessor_path`. +/// +/// [`apply`]: InsertionWitness::apply #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InsertionWitness { - /// Predecessor leaf before insertion. + /// Predecessor leaf before insertion: `predecessor.value < v < predecessor.next_value`. pub predecessor: IndexedLeaf, - /// Merkle path for the predecessor leaf, valid against the *insertion root*. + /// Merkle path for the predecessor, valid against the *insertion root*. /// - /// The insertion root equals `old_root` when `grew = false`, or - /// `H(old_root, ZEROS[d - 1])` when `grew = true` (where - /// `d = predecessor_path.len()`). + /// The insertion root is `old_root` when `grew = false`, or + /// `H(old_root, ZEROS[old_depth])` when `grew = true` + /// (where `old_depth = predecessor_path.len() - 1`). pub predecessor_path: MerklePath, - /// Merkle path for the new leaf `(v → hi)`, valid against the *intermediate - /// root* (after updating the predecessor). + /// Merkle path for the new leaf `(v → hi)`, valid against the + /// *intermediate root* produced after rewriting the predecessor. pub new_leaf_path: MerklePath, - /// `true` if the tree depth increased by 1 to fit the new leaf. + /// `true` if the tree depth increased by 1 to accommodate the new leaf. pub grew: bool, } impl InsertionWitness { - /// Verifies non-membership of `value`, applies the insertion, and returns - /// the new Merkle root. + /// Verifies non-membership of `value`, applies the two-step insertion, and + /// returns the new Merkle root. + /// + /// Steps performed in the circuit: + /// + /// 1. **Non-membership**: check `predecessor.value < value < predecessor.next_value`. + /// 2. **Growth**: if `grew`, derive `insertion_root = H(old_root, ZEROS[old_depth])`. + /// Otherwise `insertion_root = old_root`. + /// 3. **Predecessor authentication**: verify `predecessor_path` against + /// `insertion_root`. + /// 4. **Predecessor update**: compute `intermediate_root` by rehashing the + /// path with the updated predecessor `(lo → value)`. + /// 5. **Empty slot check**: verify `new_leaf_path` points to a padding leaf + /// in the tree at `intermediate_root`. + /// 6. **New leaf**: return the root obtained by placing `(value → hi)` at + /// that slot. /// - /// Returns an error if the witness is inconsistent with `old_root`. + /// Returns an error if any step fails. pub fn apply(&self, value: &Digest, old_root: &Digest) -> Result { - // --- Non-membership check --- + // Step 1 — non-membership interval check. if !digest_lt(&self.predecessor.value, value) { return Err(ArmError::ProofVerificationFailed( "insertion invalid: value ≤ predecessor value".into(), @@ -176,10 +226,11 @@ impl InsertionWitness { return Err(ArmError::NullifierDuplication); } - // --- Compute root at insertion depth (account for possible growth) --- + // Step 2 — derive the root at insertion depth. // - // When the tree grew, the insertion root is H(old_root, ZEROS[old_depth]) - // where old_depth = predecessor_path.len() - 1 (the depth *before* growth). + // When the tree grew, both paths were computed at `new_depth = old_depth + 1`. + // We reconstruct the root at that depth by hashing the old root (the entire + // left subtree) with `ZEROS[old_depth]` (an all-empty right subtree). let insertion_root = if self.grew { let old_depth = self .predecessor_path @@ -191,28 +242,29 @@ impl InsertionWitness { *old_root }; - // --- Verify predecessor exists at the insertion root --- + // Step 3 — authenticate the predecessor. if self.predecessor_path.root(&self.predecessor.hash()) != insertion_root { return Err(ArmError::ProofVerificationFailed( "insertion invalid: predecessor path does not match root".into(), )); } - // --- Compute intermediate root after updating the predecessor --- + // Step 4 — rewrite predecessor to `(lo → value)` and compute the + // intermediate root. Same path, different leaf hash. let updated_pred = IndexedLeaf { value: self.predecessor.value, next_value: *value, }; let intermediate_root = self.predecessor_path.root(&updated_pred.hash()); - // --- Verify the new leaf slot is currently empty --- + // Step 5 — verify the target slot is currently empty. if self.new_leaf_path.root(&padding_leaf()) != intermediate_root { return Err(ArmError::ProofVerificationFailed( "insertion invalid: new leaf slot is not empty".into(), )); } - // --- Append new leaf and return the new root --- + // Step 6 — place the new leaf `(value → hi)` and return the new root. let new_leaf = IndexedLeaf { value: *value, next_value: self.predecessor.next_value, @@ -223,11 +275,11 @@ impl InsertionWitness { // ── Host-side helpers ───────────────────────────────────────────────────────── -/// Builds a complete Merkle tree of the given `depth` from `leaf_hashes`. +/// Builds a complete binary Merkle tree of the given `depth` from `leaf_hashes`. /// -/// Positions `>= leaf_hashes.len()` are padded with [`padding_leaf()`]. -/// Returns `layers` where `layers[0]` is the leaf level and -/// `layers[depth]` is `[root]`. +/// Leaf positions `>= leaf_hashes.len()` are padded with [`padding_leaf()`]. +/// Returns `layers` where `layers[0]` is the leaf level (width `2^depth`) and +/// `layers[depth]` is `[root]`. Runs in O(2^depth) — host only. fn build_layers(leaf_hashes: &[Digest], depth: usize) -> Vec> { let capacity = 1usize << depth; let mut level: Vec = (0..capacity) @@ -244,7 +296,13 @@ fn build_layers(leaf_hashes: &[Digest], depth: usize) -> Vec> { layers } -/// Extracts a [`MerklePath`] for leaf `index` from a prebuilt layer array. +/// Extracts a [`MerklePath`] for the leaf at `index` from a prebuilt layer +/// array produced by [`build_layers`]. +/// +/// Each path element is `(sibling_hash, is_right_child)`, where +/// `is_right_child = true` means the *current* node is a right child (sibling +/// is to the left), matching the [`MerklePath`] encoding used by +/// [`MerklePathExt::root`]. fn extract_path(layers: &[Vec], index: usize) -> MerklePath { let depth = layers.len().saturating_sub(1); let mut path = Vec::with_capacity(depth); @@ -252,7 +310,6 @@ fn extract_path(layers: &[Vec], index: usize) -> MerklePath { for level in &layers[..depth] { let is_right_child = idx % 2 == 1; let sibling_idx = if is_right_child { idx - 1 } else { idx + 1 }; - // second element: true = sibling is to the left (current is right child) path.push((level[sibling_idx], is_right_child)); idx /= 2; } @@ -262,31 +319,33 @@ fn extract_path(layers: &[Vec], index: usize) -> MerklePath { // ── Host-side tree ──────────────────────────────────────────────────────────── /// Host-side indexed Merkle tree: generates [`NonMembershipProof`]s and -/// [`InsertionWitness`]es. +/// [`InsertionWitness`]es to be verified by the execution proof circuit. /// -/// Leaves are stored in **insertion order** (not sorted); the sorted linked -/// list is maintained through each leaf's `next_value` pointer. Path -/// generation rebuilds the full Merkle tree at the current depth, which is -/// O(2^depth) but runs only on the host. +/// Leaves are stored in **insertion order** (not sorted by value); the sorted +/// linked list is maintained through each leaf's `next_value` pointer. Path +/// generation rebuilds the full tree from scratch on every call — O(2^depth) +/// — but this runs only on the host, never inside the circuit. /// -/// A secondary `sorted_index` — a `Vec<(Digest, usize)>` kept sorted by -/// `Digest` value — enables O(log n) predecessor lookup via binary search. +/// A `sorted_index` [`BTreeMap`] keyed by [`DigestKey`] provides O(log n) +/// predecessor lookup and O(log n) insertion (no element shifting). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedMerkleTree { - /// Physical leaf array (insertion order). + /// Physical leaf array in insertion order. Index 0 is always the + /// `MIN_VALUE → MAX_VALUE` sentinel. leaves: Vec, - /// Current Merkle tree depth. + /// Current Merkle tree depth. Grows automatically when `leaves.len()` + /// would exceed `2^depth`. depth: usize, - /// Sorted index: `(leaf.value, physical_index_in_leaves)`, ordered by - /// value using the same big-endian word comparison as [`digest_lt`]. - sorted_index: Vec<(Digest, usize)>, + /// Maps each leaf value to its physical index in `leaves`, kept in sorted + /// order via [`DigestKey`] for O(log n) predecessor range queries. + sorted_index: BTreeMap, } impl IndexedMerkleTree { /// Creates a new empty indexed Merkle tree. /// - /// Inserts the initial sentinel leaf `(MIN_VALUE → MAX_VALUE)` so that - /// every real value has a valid predecessor. + /// Seeds the tree with the sentinel leaf `(MIN_VALUE → MAX_VALUE)` at + /// physical index 0, so every insertable value has a valid predecessor. pub fn new() -> Self { Self { leaves: vec![IndexedLeaf { @@ -294,33 +353,37 @@ impl IndexedMerkleTree { next_value: *MAX_VALUE, }], depth: 0, - sorted_index: vec![(*MIN_VALUE, 0)], + sorted_index: BTreeMap::from([(DigestKey(*MIN_VALUE), 0)]), } } /// Returns the current Merkle root. pub fn root(&self) -> Digest { let hashes: Vec = self.leaves.iter().map(|l| l.hash()).collect(); - // build_layers always returns at least one layer with at least one element. + // build_layers always returns depth+1 layers, each non-empty. build_layers(&hashes, self.depth)[self.depth][0] } - /// Returns the number of leaves (including the [`MIN_VALUE`] sentinel). + /// Returns the number of leaves, including the [`MIN_VALUE`] sentinel. pub fn len(&self) -> usize { self.leaves.len() } - /// Returns `true` if the tree contains only the sentinel leaf. + /// Returns `true` if the tree contains only the sentinel leaf (no real + /// values have been inserted). pub fn is_empty(&self) -> bool { self.leaves.len() == 1 } - /// Generates a [`NonMembershipProof`] for `value`. + /// Generates a [`NonMembershipProof`] showing that `value` is not in the + /// tree. /// - /// Returns an error if `value` is already in the set or equals [`MIN_VALUE`]. + /// Returns an error if `value` is already in the set or equals + /// [`MIN_VALUE`] (which has no valid predecessor). pub fn prove_non_membership(&self, value: &Digest) -> Result { let pred_idx = self.predecessor_index(value)?; let predecessor = self.leaves[pred_idx].clone(); + // If value >= predecessor.next_value, value is already in the set. if !digest_lt(value, &predecessor.next_value) { return Err(ArmError::NullifierDuplication); } @@ -332,7 +395,7 @@ impl IndexedMerkleTree { }) } - /// Inserts `value` and returns the [`InsertionWitness`]. + /// Inserts `value` into the tree and returns the [`InsertionWitness`]. /// /// Returns an error if `value` is already in the set, equals [`MIN_VALUE`], /// or equals [`MAX_VALUE`]. @@ -350,16 +413,18 @@ impl IndexedMerkleTree { return Err(ArmError::NullifierDuplication); } - let n = self.leaves.len(); + let n = self.leaves.len(); // physical index of the new leaf let grew = n + 1 > (1 << self.depth); let new_depth = if grew { self.depth + 1 } else { self.depth }; - // predecessor_path at new_depth against the insertion root + // Build the tree at new_depth. When grew=true this is the "insertion + // root" depth; the extra right-subtree slots are all padding leaves. let current_hashes: Vec = self.leaves.iter().map(|l| l.hash()).collect(); let tree_before = build_layers(¤t_hashes, new_depth); let predecessor_path = extract_path(&tree_before, pred_idx); - // rebuild with updated predecessor, then get new_leaf_path + // Rewrite the predecessor leaf hash and rebuild to get the intermediate + // root (after step 1 of the two-step insertion). let mut updated_hashes = current_hashes; updated_hashes[pred_idx] = IndexedLeaf { value: predecessor.value, @@ -367,19 +432,17 @@ impl IndexedMerkleTree { } .hash(); let tree_after_update = build_layers(&updated_hashes, new_depth); + // The new leaf lands at index n; its slot is currently a padding leaf. let new_leaf_path = extract_path(&tree_after_update, n); - // apply mutation + // Commit mutations. self.leaves[pred_idx].next_value = value; self.leaves.push(IndexedLeaf { value, next_value: predecessor.next_value, }); self.depth = new_depth; - let ins = self - .sorted_index - .partition_point(|(v, _)| digest_lt(v, &value)); - self.sorted_index.insert(ins, (value, n)); + self.sorted_index.insert(DigestKey(value), n); Ok(InsertionWitness { predecessor, @@ -389,21 +452,19 @@ impl IndexedMerkleTree { }) } - /// Returns the physical index of the predecessor leaf for `value`. + /// Returns the physical index in `leaves` of the predecessor of `value`. + /// + /// The predecessor is the leaf whose value is the largest value strictly + /// less than `value`. Uses `BTreeMap::range(..key).next_back()` — O(log n). /// - /// Uses binary search on `sorted_index` (O(log n)): finds the last entry - /// whose value is strictly less than `value`. + /// Returns [`ArmError::InvalidLeaf`] if no predecessor exists (i.e. + /// `value ≤ MIN_VALUE`). fn predecessor_index(&self, value: &Digest) -> Result { - // partition_point returns the first position where the predicate is - // false, i.e. the first entry with value >= target. The predecessor - // is the entry immediately before it. - let pos = self - .sorted_index - .partition_point(|(v, _)| digest_lt(v, value)); - if pos == 0 { - return Err(ArmError::InvalidLeaf); - } - Ok(self.sorted_index[pos - 1].1) + self.sorted_index + .range(..DigestKey(*value)) + .next_back() + .map(|(_, &idx)| idx) + .ok_or(ArmError::InvalidLeaf) } } @@ -443,19 +504,9 @@ mod tests { #[test] fn leaf_hash_is_deterministic() { - let l = IndexedLeaf { - value: d(10), - next_value: d(20), - }; + let l = IndexedLeaf { value: d(10), next_value: d(20) }; assert_eq!(l.hash(), l.hash()); - assert_ne!( - l.hash(), - IndexedLeaf { - value: d(10), - next_value: d(30) - } - .hash() - ); + assert_ne!(l.hash(), IndexedLeaf { value: d(10), next_value: d(30) }.hash()); } // ── non-membership proof ───────────────────────────────────────────────── @@ -474,7 +525,7 @@ mod tests { tree.insert(d(10)).unwrap(); tree.insert(d(30)).unwrap(); - // 20 is between 10 and 30 → not in set + // 20 is strictly between 10 and 30 → not in the set let target = d(20); let proof = tree.prove_non_membership(&target).unwrap(); proof.verify(&target, &tree.root()).unwrap(); @@ -521,13 +572,13 @@ mod tests { let nm_proof = tree.prove_non_membership(&target).unwrap(); nm_proof.verify(&target, &tree.root()).unwrap(); - // inserting the same value changes the root + // Inserting the value changes the root. let old_root = tree.root(); let witness = tree.insert(target).unwrap(); let new_root = tree.root(); assert_eq!(witness.apply(&target, &old_root).unwrap(), new_root); - // 20 is now in the set; non-membership for 25 should still work + // 20 is now in the set; non-membership for 25 (between 20 and 30) still works. let proof2 = tree.prove_non_membership(&d(25)).unwrap(); proof2.verify(&d(25), &tree.root()).unwrap(); } @@ -535,17 +586,20 @@ mod tests { #[test] fn insertion_triggers_tree_growth() { let mut tree = IndexedMerkleTree::new(); - // depth=0 → capacity=1 (full with sentinel); first insert must grow - let witness = tree.insert(d(1)).unwrap(); - assert!(witness.grew); + + // depth=0, capacity=1. The sentinel occupies the only slot, so the + // first real insertion must grow the tree to depth=1 (capacity=2). + let w1 = tree.insert(d(1)).unwrap(); + assert!(w1.grew); assert_eq!(tree.depth, 1); - // second insert stays at depth=1 (capacity=2, now has 3 leaves after sentinel) - // actually depth=1 → capacity=2; with sentinel + 1 we have 2, full again + // depth=1, capacity=2, leaves=[sentinel, d(1)]. The next insertion + // needs a third slot, so the tree must grow again to depth=2. let old_root = tree.root(); - let witness2 = tree.insert(d(2)).unwrap(); - assert!(witness2.grew); // 2+1=3 > 2^1=2 → must grow to depth 2 - assert_eq!(witness2.apply(&d(2), &old_root).unwrap(), tree.root()); + let w2 = tree.insert(d(2)).unwrap(); + assert!(w2.grew); + assert_eq!(tree.depth, 2); + assert_eq!(w2.apply(&d(2), &old_root).unwrap(), tree.root()); } // ── build_layers / extract_path consistency ─────────────────────────────── @@ -554,7 +608,10 @@ mod tests { fn build_layers_root_matches_direct_hash() { let leaves = vec![d(1), d(2), d(3), d(4)]; let layers = build_layers(&leaves, 2); - let expected = hash_pair(&hash_pair(&d(1), &d(2)), &hash_pair(&d(3), &d(4))); + let expected = hash_pair( + &hash_pair(&d(1), &d(2)), + &hash_pair(&d(3), &d(4)), + ); assert_eq!(*layers.last().unwrap().first().unwrap(), expected); } From 4eed339ec6f3888896331e02f3e94a492b9b4e4d Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Wed, 25 Mar 2026 21:58:17 +0800 Subject: [PATCH 07/21] replace new_commitment_root with new_commitment_tree in ExecutionProofInstance --- arm/src/execution_proof.rs | 8 ++------ arm/src/indexed_merkle_tree.rs | 19 +++++++++++++------ .../execution_proof/methods/guest/src/main.rs | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 7f5a2029..0c9b2997 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -33,12 +33,8 @@ pub struct ExecutionProofInstance { pub old_commitment_tree_root: Digest, /// Nullifier tree root before the batch was executed. pub old_nullifier_tree_root: Digest, - /// Full commitment tree state after executing the batch. - /// - /// Carries the incremental tree so the next proof can continue inserting - /// commitments without re-proving the prior state. - /// `new_commitment_tree.root()` gives the updated commitment root. - pub new_commitment_tree: IncrementalMerkleTree, + /// Commitment tree root after executing the batch. + pub new_commitment_root: Digest, /// Nullifier tree root after executing the batch. pub new_nullifier_tree_root: Digest, } diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs index 63eec364..cc9c705c 100644 --- a/arm/src/indexed_merkle_tree.rs +++ b/arm/src/indexed_merkle_tree.rs @@ -504,9 +504,19 @@ mod tests { #[test] fn leaf_hash_is_deterministic() { - let l = IndexedLeaf { value: d(10), next_value: d(20) }; + let l = IndexedLeaf { + value: d(10), + next_value: d(20), + }; assert_eq!(l.hash(), l.hash()); - assert_ne!(l.hash(), IndexedLeaf { value: d(10), next_value: d(30) }.hash()); + assert_ne!( + l.hash(), + IndexedLeaf { + value: d(10), + next_value: d(30) + } + .hash() + ); } // ── non-membership proof ───────────────────────────────────────────────── @@ -608,10 +618,7 @@ mod tests { fn build_layers_root_matches_direct_hash() { let leaves = vec![d(1), d(2), d(3), d(4)]; let layers = build_layers(&leaves, 2); - let expected = hash_pair( - &hash_pair(&d(1), &d(2)), - &hash_pair(&d(3), &d(4)), - ); + let expected = hash_pair(&hash_pair(&d(1), &d(2)), &hash_pair(&d(3), &d(4))); assert_eq!(*layers.last().unwrap().first().unwrap(), expected); } diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index 8e58711f..b7da68d0 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -164,7 +164,7 @@ pub fn main() { env::commit(&ExecutionProofInstance { old_commitment_tree_root, old_nullifier_tree_root: witness.old_nullifier_tree_root, - new_commitment_tree: commitment_tree, + new_commitment_root: commitment_tree.root(), new_nullifier_tree_root: nullifier_root, }); } From 60ba61cc581341f4a8f6ac296f53cbbc418f4f1d Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Wed, 25 Mar 2026 22:49:54 +0800 Subject: [PATCH 08/21] remove the execution_circuit feature flag --- arm/Cargo.toml | 1 - arm/src/lib.rs | 1 - arm_circuits/execution_proof/Cargo.toml | 2 +- arm_circuits/execution_proof/methods/guest/Cargo.toml | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/arm/Cargo.toml b/arm/Cargo.toml index a2be8d0c..ef3ebe3e 100644 --- a/arm/Cargo.toml +++ b/arm/Cargo.toml @@ -42,4 +42,3 @@ bonsai = ["risc0-zkvm/bonsai"] cuda = ["risc0-zkvm/cuda"] aggregation = ["aggregation_circuit", "transaction"] aggregation_circuit = [] -execution_circuit = ["compliance_circuit", "transaction"] diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 3bfd9723..94b2fd99 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -14,7 +14,6 @@ pub mod constants; #[cfg(feature = "transaction")] pub mod delta_proof; pub mod error; -#[cfg(feature = "execution_circuit")] pub mod execution_proof; pub mod incremental_merkle_tree; pub mod indexed_merkle_tree; diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml index 1449cc21..12111f93 100644 --- a/arm_circuits/execution_proof/Cargo.toml +++ b/arm_circuits/execution_proof/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" execution-proof-methods = { path = "methods" } risc0-zkvm = "3.0.3" anoma-rm-risc0 = { path = "../../arm", features = [ - "execution_circuit", + "transaction", ], default-features = false } bincode = "1.3.3" diff --git a/arm_circuits/execution_proof/methods/guest/Cargo.toml b/arm_circuits/execution_proof/methods/guest/Cargo.toml index ed7dd4b8..de210036 100644 --- a/arm_circuits/execution_proof/methods/guest/Cargo.toml +++ b/arm_circuits/execution_proof/methods/guest/Cargo.toml @@ -11,7 +11,7 @@ risc0-zkvm = { version = "3.0.3", features = [ "unstable", ], default-features = false } anoma-rm-risc0 = { path = "../../../../arm", features = [ - "execution_circuit", + "transaction", ], default-features = false } arm-core = { package = "anoma-rm-core", path = "../../../../arm_core" } From f46988d5bf1bfc4aeec0b35ab0f16878895dfde1 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Thu, 26 Mar 2026 22:54:37 +0800 Subject: [PATCH 09/21] execution proof tests --- arm_circuits/Cargo.lock | 13 ++ arm_circuits/execution_proof/Cargo.toml | 4 + arm_circuits/execution_proof/src/main.rs | 242 +++++++++++++++++++++++ arm_core/src/delta_types.rs | 18 +- 4 files changed, 275 insertions(+), 2 deletions(-) diff --git a/arm_circuits/Cargo.lock b/arm_circuits/Cargo.lock index aa5fbe46..3f178933 100644 --- a/arm_circuits/Cargo.lock +++ b/arm_circuits/Cargo.lock @@ -281,6 +281,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "anoma-rm-risc0-test-app" +version = "1.1.1" +dependencies = [ + "anoma-rm-risc0", + "anoma-rm-risc0-test-witness", + "hex", + "k256", + "lazy_static", + "serde", +] + [[package]] name = "anoma-rm-risc0-test-witness" version = "1.1.1" @@ -1742,6 +1754,7 @@ name = "execution-proof" version = "1.0.0" dependencies = [ "anoma-rm-risc0", + "anoma-rm-risc0-test-app", "bincode", "execution-proof-methods", "risc0-zkvm", diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml index 12111f93..3ce0c47d 100644 --- a/arm_circuits/execution_proof/Cargo.toml +++ b/arm_circuits/execution_proof/Cargo.toml @@ -11,6 +11,10 @@ anoma-rm-risc0 = { path = "../../arm", features = [ ], default-features = false } bincode = "1.3.3" +[dev-dependencies] +anoma-rm-risc0 = { path = "../../arm", features = ["aggregation", "prove"] } +anoma-rm-risc0-test-app = { path = "../../arm_tests/arm_test_app" } + [features] default = [] cuda = ["risc0-zkvm/cuda"] diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 68f10192..13fdd9ea 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -12,6 +12,7 @@ pub fn prove( witness: &ExecutionProofWitness, proof_type: risc0_zkvm::ProverOpts, ) -> Result> { + use anoma_rm_risc0::TransactionExt; use execution_proof_methods::EXECUTION_PROOF_GUEST_ELF; use risc0_zkvm::{default_prover, ExecutorEnv, InnerReceipt, VerifierContext}; @@ -47,6 +48,247 @@ pub fn prove( Ok(receipt) } +#[cfg(test)] +mod tests { + use anoma_rm_risc0::{ + execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, + incremental_merkle_tree::IncrementalMerkleTree, + indexed_merkle_tree::IndexedMerkleTree, + Digest, + }; + + fn empty_witness() -> ExecutionProofWitness { + let nullifier_tree = IndexedMerkleTree::new(); + ExecutionProofWitness { + transactions: vec![], + commitment_tree: IncrementalMerkleTree::new(3), + old_nullifier_tree_root: nullifier_tree.root(), + nullifier_witnesses: vec![], + } + } + + // ── Structural tests (no prove feature required) ────────────────────────── + + #[test] + fn witness_serde_roundtrip() { + let witness = empty_witness(); + let encoded = bincode::serialize(&witness).unwrap(); + let decoded: ExecutionProofWitness = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded.transactions.len(), 0); + assert_eq!(decoded.nullifier_witnesses.len(), 0); + assert_eq!(decoded.commitment_tree, witness.commitment_tree); + assert_eq!( + decoded.old_nullifier_tree_root, + witness.old_nullifier_tree_root + ); + } + + #[test] + fn instance_serde_roundtrip() { + let d = |v: u32| Digest::new([v, 0, 0, 0, 0, 0, 0, 0]); + let instance = ExecutionProofInstance { + old_commitment_tree_root: d(1), + old_nullifier_tree_root: d(2), + new_commitment_root: d(3), + new_nullifier_tree_root: d(4), + }; + let encoded = bincode::serialize(&instance).unwrap(); + let decoded: ExecutionProofInstance = bincode::deserialize(&encoded).unwrap(); + assert_eq!(instance, decoded); + } + + /// Verifies the pattern used in witness construction: one InsertionWitness + /// per nullifier, threading roots through `InsertionWitness::apply`. + #[test] + fn nullifier_witnesses_thread_roots_correctly() { + // Use arbitrary distinct digests as nullifiers — no real transaction needed. + let nullifiers: Vec = (1u32..=4) + .map(|i| Digest::new([i, 0, 0, 0, 0, 0, 0, 0])) + .collect(); + + let mut nullifier_tree = IndexedMerkleTree::new(); + let mut current_root = nullifier_tree.root(); + + for &nf in &nullifiers { + let witness = nullifier_tree.insert(nf).unwrap(); + current_root = witness.apply(&nf, ¤t_root).unwrap(); + } + + // After applying all witnesses the threaded root must equal the tree root. + assert_eq!(current_root, nullifier_tree.root()); + } + + /// Confirms that inserting duplicate nullifiers into the indexed tree fails, + /// which is the property the circuit depends on for batch uniqueness. + #[test] + fn duplicate_nullifier_insertion_fails() { + let nf = Digest::new([42, 0, 0, 0, 0, 0, 0, 0]); + let mut nullifier_tree = IndexedMerkleTree::new(); + nullifier_tree.insert(nf).unwrap(); + assert!(nullifier_tree.insert(nf).is_err()); + } + + // ── Prove tests (require --features prove) ──────────────────────────────── + + /// Proves an empty batch and checks that the committed instance records + /// the correct (unchanged) tree roots. + /// + /// Run with: `RISC0_DEV_MODE=1 cargo test --features prove prove_empty_batch` + #[cfg(feature = "prove")] + #[test] + fn prove_empty_batch() { + let commitment_tree = IncrementalMerkleTree::new(3); + let nullifier_tree = IndexedMerkleTree::new(); + let old_commitment_root = commitment_tree.root(); + let old_nullifier_root = nullifier_tree.root(); + + let witness = ExecutionProofWitness { + transactions: vec![], + commitment_tree, + old_nullifier_tree_root: old_nullifier_root, + nullifier_witnesses: vec![], + }; + + let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); + + let instance: ExecutionProofInstance = receipt.journal.decode().unwrap(); + // No transactions — both tree roots must be unchanged. + assert_eq!(instance.old_commitment_tree_root, old_commitment_root); + assert_eq!(instance.old_nullifier_tree_root, old_nullifier_root); + assert_eq!(instance.new_commitment_root, old_commitment_root); + assert_eq!(instance.new_nullifier_tree_root, old_nullifier_root); + + super::verify(&receipt).unwrap(); + } + + /// Verifying a valid receipt against the wrong image ID must fail. + /// + /// Run with: `RISC0_DEV_MODE=1 cargo test --features prove verify_rejects_wrong_image_id` + #[cfg(feature = "prove")] + #[test] + fn verify_rejects_wrong_image_id() { + let receipt = super::prove(&empty_witness(), risc0_zkvm::ProverOpts::succinct()).unwrap(); + // All-zeros is never a valid image ID. + assert!(receipt.verify([0u32; 8]).is_err()); + } + + /// Proves a single aggregated transaction and checks that the committed + /// roots differ from their initial values (one commitment and one nullifier + /// were inserted). + /// + /// Run with: + /// RISC0_DEV_MODE=1 cargo test --features prove prove_single_aggregated_transaction + #[cfg(feature = "prove")] + #[test] + fn prove_single_aggregated_transaction() { + use anoma_rm_risc0::{proving_system::ProofType, TransactionExt}; + use anoma_rm_risc0_test_app::generate_test_transaction; + + let mut tx = generate_test_transaction(1, 1, ProofType::Succinct); + tx.aggregate(ProofType::Succinct).unwrap(); + + let mut nullifier_tree = IndexedMerkleTree::new(); + let mut nullifier_witnesses = Vec::new(); + for action in &tx.actions { + for cu in action.get_compliance_units() { + let w = nullifier_tree + .insert(cu.instance.consumed_nullifier) + .unwrap(); + nullifier_witnesses.push(w); + } + } + + let commitment_tree = IncrementalMerkleTree::new(3); + let old_nullifier_root = IndexedMerkleTree::new().root(); + let old_commitment_root = commitment_tree.root(); + + let witness = ExecutionProofWitness { + transactions: vec![tx], + commitment_tree, + old_nullifier_tree_root: old_nullifier_root, + nullifier_witnesses, + }; + + let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); + + let instance: ExecutionProofInstance = receipt.journal.decode().unwrap(); + assert_eq!(instance.old_commitment_tree_root, old_commitment_root); + assert_eq!(instance.old_nullifier_tree_root, old_nullifier_root); + // After one compliance unit the trees must have advanced. + assert_ne!(instance.new_commitment_root, old_commitment_root); + assert_ne!(instance.new_nullifier_tree_root, old_nullifier_root); + + super::verify(&receipt).unwrap(); + } + + /// Proves a batch of two independent transactions and checks that + /// `verify` accepts the receipt. + /// + /// Run with: + /// RISC0_DEV_MODE=1 cargo test --features prove prove_two_aggregated_transactions + #[cfg(feature = "prove")] + #[test] + fn prove_two_aggregated_transactions() { + use anoma_rm_risc0::{ + proving_system::ProofType, + transaction::{Delta, Transaction}, + CoreDeltaWitness, TransactionExt, + }; + use anoma_rm_risc0_test_app::create_an_action_with_multiple_compliances; + + // Build each transaction with a distinct nonce (0 and 1) so their + // compliance units produce different resources and unique nullifiers. + let (action1, dw1) = create_an_action_with_multiple_compliances(1, 0, ProofType::Succinct); + let mut tx1 = Transaction::create( + vec![action1], + Delta::Witness(CoreDeltaWitness(dw1.to_bytes())), + ) + .generate_delta_proof() + .unwrap(); + tx1.aggregate(ProofType::Succinct).unwrap(); + + let (action2, dw2) = create_an_action_with_multiple_compliances(1, 1, ProofType::Succinct); + let mut tx2 = Transaction::create( + vec![action2], + Delta::Witness(CoreDeltaWitness(dw2.to_bytes())), + ) + .generate_delta_proof() + .unwrap(); + tx2.aggregate(ProofType::Succinct).unwrap(); + + // Build nullifier insertion witnesses for all compliance units in batch order. + let mut nullifier_tree = IndexedMerkleTree::new(); + let mut nullifier_witnesses = Vec::new(); + for tx in [&tx1, &tx2] { + for action in &tx.actions { + for cu in action.get_compliance_units() { + let w = nullifier_tree + .insert(cu.instance.consumed_nullifier) + .unwrap(); + nullifier_witnesses.push(w); + } + } + } + + let commitment_tree = IncrementalMerkleTree::new(3); + let old_nullifier_root = IndexedMerkleTree::new().root(); + + let witness = ExecutionProofWitness { + transactions: vec![tx1, tx2], + commitment_tree, + old_nullifier_tree_root: old_nullifier_root, + nullifier_witnesses, + }; + + let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); + + let instance: ExecutionProofInstance = receipt.journal.decode().unwrap(); + assert_eq!(instance.old_nullifier_tree_root, old_nullifier_root); + + super::verify(&receipt).unwrap(); + } +} + // Updates the ELF binary and prints the image ID. // Run with: cargo test --features prove print_execution_proof_elf_id -- --nocapture #[test] diff --git a/arm_core/src/delta_types.rs b/arm_core/src/delta_types.rs index efe4dd4c..c68a49f2 100644 --- a/arm_core/src/delta_types.rs +++ b/arm_core/src/delta_types.rs @@ -12,7 +12,15 @@ pub struct DeltaWitness(pub [u8; 32]); impl Serialize for DeltaProof { fn serialize(&self, s: S) -> Result { - s.serialize_bytes(&self.0) + // Use serialize_seq so that RISC0 serde (which packs 4 bytes per u32 + // word in serialize_bytes but stores 1 byte per word in serialize_seq) + // stays consistent with Vec::deserialize on the guest side. + // Bincode treats serialize_bytes and serialize_seq(u8) identically. + let mut seq = s.serialize_seq(Some(self.0.len()))?; + for byte in &self.0 { + serde::ser::SerializeSeq::serialize_element(&mut seq, byte)?; + } + serde::ser::SerializeSeq::end(seq) } } @@ -27,7 +35,13 @@ impl<'de> Deserialize<'de> for DeltaProof { impl Serialize for DeltaWitness { fn serialize(&self, s: S) -> Result { - s.serialize_bytes(&self.0) + // Same rationale as DeltaProof: use serialize_seq for RISC0 serde + // compatibility while remaining wire-compatible with bincode. + let mut seq = s.serialize_seq(Some(self.0.len()))?; + for byte in &self.0 { + serde::ser::SerializeSeq::serialize_element(&mut seq, byte)?; + } + serde::ser::SerializeSeq::end(seq) } } From c4e71642c774419f724c20d85dc45bf2d9318d7a Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Mon, 30 Mar 2026 13:58:12 +0800 Subject: [PATCH 10/21] add app data to ExecutionProofInstance --- arm/src/execution_proof.rs | 19 +- .../execution_proof/methods/guest/src/main.rs | 240 +++++++++++++----- arm_circuits/execution_proof/src/main.rs | 2 + 3 files changed, 197 insertions(+), 64 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 0c9b2997..8ac8ee54 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -17,11 +17,22 @@ //! incremental commitment tree. use crate::{ - incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, Digest, - Transaction, + incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, AppData, + Digest, Transaction, }; use serde::{Deserialize, Serialize}; +/// Application data associated with a single resource in the execution proof. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ResourceAppData { + /// The resource tag (nullifier for consumed, commitment for created). + pub tag: Digest, + /// The verifying key of the resource's logic proof. + pub vk: Digest, + /// The application data payload for this resource. + pub app_data: AppData, +} + /// Public outputs of the execution proof, committed to the journal. /// /// Downstream verifiers chain proofs by checking that @@ -37,6 +48,10 @@ pub struct ExecutionProofInstance { pub new_commitment_root: Digest, /// Nullifier tree root after executing the batch. pub new_nullifier_tree_root: Digest, + /// Application data for each consumed resource in this execution batch. + pub consumed_resource_app_data: Vec, + /// Application data for each created resource in this execution batch. + pub created_resource_app_data: Vec, } /// Private witness consumed by the execution proof circuit. diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index b7da68d0..dcab784a 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -1,14 +1,14 @@ use anoma_rm_risc0::{ + action::Action, + action_tree::MerkleTree, compliance::ComplianceInstanceWords, constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, delta_proof::DeltaProof, - execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, + execution_proof::{ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, transaction::{Delta, TransactionExt}, - utils::bytes_to_words, - Digest, + Digest, LogicInstance, }; use risc0_zkvm::guest::env; -use std::collections::HashSet; /// Converts a 32-byte VK constant to the `risc0_zkvm::sha::Digest` expected by `env::verify`. fn vk_to_risc0(bytes: &[u8; 32]) -> risc0_zkvm::sha::Digest { @@ -18,15 +18,112 @@ fn vk_to_risc0(bytes: &[u8; 32]) -> risc0_zkvm::sha::Digest { risc0_zkvm::sha::Digest::new(words) } +/// Output of [`collect_action_logic`]: the serialised logic proof data needed +/// for the aggregation instance, plus the resource app-data split by role. +struct ActionLogicData { + /// Serialised logic instances, one per resource (consumed then created per CU). + lp_instances_u32: Vec>, + /// Verifying keys parallel to `lp_instances_u32`. + lp_vks: Vec, + /// App-data for consumed resources (nullifier tags), in CU order. + consumed_resource_app_data: Vec, + /// App-data for created resources (commitment tags), in CU order. + created_resource_app_data: Vec, +} + +/// Processes a single action in one pass over its compliance units: +/// +/// - Builds the ordered tag / logic-ref lists for the action tree. +/// - Asserts that each input's `verifying_key` matches the corresponding +/// `logic_ref` committed inside the compliance instance. +/// - Serialises each [`LogicInstance`] for the aggregation proof. +/// - Splits [`ResourceAppData`] into consumed and created buckets. +fn collect_action_logic(action: &Action) -> ActionLogicData { + let mut tags = Vec::new(); + let mut logic_refs = Vec::new(); + + for cu in action.get_compliance_units() { + // Ordered as [consumed, created] per CU to match proof construction. + tags.push(cu.instance.consumed_nullifier); + logic_refs.push(cu.instance.consumed_logic_ref); + tags.push(cu.instance.created_commitment); + logic_refs.push(cu.instance.created_logic_ref); + } + + let root = MerkleTree::from(tags.clone()) + .root() + .expect("action tree root"); + + let mut lp_vks = Vec::new(); + let mut lp_instances_u32 = Vec::new(); + let mut consumed_resource_app_data = Vec::new(); + let mut created_resource_app_data = Vec::new(); + + for (index, (tag, logic_ref)) in tags.iter().zip(logic_refs.iter()).enumerate() { + let is_consumed = index % 2 == 0; + + let input = action + .get_logic_verifier_inputs() + .iter() + .find(|i| i.tag == *tag) + .expect("logic verifier input not found for tag"); + + assert_eq!( + input.verifying_key, *logic_ref, + "verifying key does not match logic ref for tag" + ); + + lp_instances_u32.push( + risc0_zkvm::serde::to_vec(&LogicInstance { + tag: input.tag, + is_consumed, + root, + app_data: input.app_data.clone(), + }) + .expect("serialize logic instance"), + ); + lp_vks.push(input.verifying_key); + + let resource_app_data = ResourceAppData { + tag: input.tag, + vk: input.verifying_key, + app_data: input.app_data.clone(), + }; + if is_consumed { + consumed_resource_app_data.push(resource_app_data); + } else { + created_resource_app_data.push(resource_app_data); + } + } + + ActionLogicData { + lp_instances_u32, + lp_vks, + consumed_resource_app_data, + created_resource_app_data, + } +} + +/// All data produced by [`aggregation_instance_words`] for one transaction. +struct TxVerificationData { + /// Serialised aggregation instance ready for `env::verify`. + agg_words: Vec, + /// App-data for consumed resources across all actions. + consumed_resource_app_data: Vec, + /// App-data for created resources across all actions. + created_resource_app_data: Vec, +} + /// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. /// -/// Replicates `TransactionExt::construct_aggregation_instance` without -/// requiring the host-only `aggregation` feature flag (which pulls in the -/// RISC0 prover stack). +/// Delegates per-action work to [`collect_action_logic`], which visits each +/// action's compliance units and `logic_verifier_inputs` exactly once. +/// The returned [`TxVerificationData`] carries everything needed for tree +/// updates and the final instance, so callers need no further iteration. fn aggregation_instance_words( tx: &anoma_rm_risc0::Transaction, compliance_vk: &Digest, -) -> Vec { +) -> TxVerificationData { let compliance_instances_u32: Vec = tx .get_compliance_instances() .expect("compliance instances") @@ -34,22 +131,32 @@ fn aggregation_instance_words( .map(|b| ComplianceInstanceWords::from_bytes(b).expect("compliance instance words")) .collect(); - let (lp_vks, lp_instances) = tx - .get_logic_vks_and_instances() - .expect("logic vks and instances"); + let mut lp_vks = Vec::new(); + let mut lp_instances_u32 = Vec::new(); + let mut consumed_resource_app_data = Vec::new(); + let mut created_resource_app_data = Vec::new(); - let lp_instances_u32: Vec> = lp_instances - .iter() - .map(|b| bytes_to_words(b)) - .collect(); + for action in &tx.actions { + let data = collect_action_logic(action); + lp_vks.extend(data.lp_vks); + lp_instances_u32.extend(data.lp_instances_u32); + consumed_resource_app_data.extend(data.consumed_resource_app_data); + created_resource_app_data.extend(data.created_resource_app_data); + } - risc0_zkvm::serde::to_vec(&( + let agg_words = risc0_zkvm::serde::to_vec(&( compliance_instances_u32, compliance_vk, lp_instances_u32, lp_vks, )) - .expect("serialize aggregation instance") + .expect("serialize aggregation instance"); + + TxVerificationData { + agg_words, + consumed_resource_app_data, + created_resource_app_data, + } } pub fn main() { @@ -65,10 +172,6 @@ pub fn main() { let old_commitment_tree_root = commitment_tree.root(); let mut nullifier_root = witness.old_nullifier_tree_root; - // Flat cursor into `witness.nullifier_witnesses`, advanced once per - // compliance unit across all transactions and actions. - let mut nullifier_witness_idx: usize = 0; - // ----------------------------------------------------------------------- // 2. Batch-wide nullifier uniqueness check. // @@ -76,19 +179,33 @@ pub fn main() { // that was inserted in a prior batch. This check additionally prevents // two compliance units *within this batch* from consuming the same // nullifier before any of them reach the tree-update step. + // + // Nullifiers and commitments are collected here for reuse in the + // tree-update step, avoiding a second pass over the transactions. // ----------------------------------------------------------------------- - let mut seen_nullifiers = HashSet::::new(); + let mut nullifiers: Vec = Vec::new(); + let mut commitments: Vec = Vec::new(); for tx in &witness.transactions { for action in &tx.actions { for cu in action.get_compliance_units() { - assert!( - seen_nullifiers.insert(cu.instance.consumed_nullifier), - "duplicate nullifier across transactions" - ); + nullifiers.push(cu.instance.consumed_nullifier); + commitments.push(cu.instance.created_commitment); } } } + // Sort a copy of the nullifiers and check adjacent pairs for duplicates. + // Sorting with integer comparisons is far cheaper in the zkVM than hashing + // with HashSet (SipHash has no RISC0 accelerator and costs many cycles per call). + let mut sorted_nullifiers = nullifiers.clone(); + sorted_nullifiers.sort_by_key(|d| *d.as_words()); + for window in sorted_nullifiers.windows(2) { + assert_ne!( + window[0], window[1], + "duplicate nullifier across transactions" + ); + } + // ----------------------------------------------------------------------- // 3. Per-transaction verification and state transition. // ----------------------------------------------------------------------- @@ -96,6 +213,9 @@ pub fn main() { let compliance_vk_core = Digest::try_from(COMPLIANCE_VK_BYTES.as_slice()).expect("compliance VK bytes"); + let mut consumed_resource_app_data: Vec = Vec::new(); + let mut created_resource_app_data: Vec = Vec::new(); + for tx in &witness.transactions { // --- 3a. Delta proof --- // @@ -105,57 +225,51 @@ pub fn main() { let delta_instance = tx.delta().expect("delta instance"); match &tx.delta_proof { Delta::Proof(core_proof) => { - let proof = - DeltaProof::from_bytes(&core_proof.0).expect("deserialize delta proof"); + let proof = DeltaProof::from_bytes(&core_proof.0).expect("deserialize delta proof"); DeltaProof::verify(&msg, &proof, delta_instance).expect("delta proof invalid"); } Delta::Witness(_) => panic!("expected delta proof, got witness"), } - // --- 3b. Batch aggregation proof --- + // --- 3b. Batch aggregation proof + data collection --- // // Confirms that every compliance proof and logic proof inside this // transaction has been verified by the batch aggregation circuit. - // Transactions without an aggregation proof are rejected. + // Nullifiers, commitments, and ResourceAppData are all collected in the + // same pass, so no further iteration over actions is needed. assert!( tx.aggregation_proof.is_some(), "transaction is missing an aggregation proof" ); - let agg_words = aggregation_instance_words(tx, &compliance_vk_core); - env::verify(batch_agg_vk_risc0, &agg_words) + let tx_data = aggregation_instance_words(tx, &compliance_vk_core); + env::verify(batch_agg_vk_risc0, &tx_data.agg_words) .expect("aggregation proof verification failed"); + consumed_resource_app_data.extend(tx_data.consumed_resource_app_data); + created_resource_app_data.extend(tx_data.created_resource_app_data); + } - // --- 3c. Per-compliance-unit tree updates --- - // - // For each compliance unit, in order: - // - // Nullifier: `InsertionWitness::apply` proves that `consumed_nullifier` - // is not yet in the indexed nullifier tree (non-membership), - // inserts it, and returns the new nullifier root. - // - // Commitment: `IncrementalMerkleTree::insert` appends `created_commitment` - // to the incremental commitment tree. - for action in &tx.actions { - for cu in action.get_compliance_units() { - let nf = cu.instance.consumed_nullifier; - let commitment = cu.instance.created_commitment; - - let nf_witness = witness - .nullifier_witnesses - .get(nullifier_witness_idx) - .expect("missing nullifier insertion witness"); - - nullifier_root = nf_witness - .apply(&nf, &nullifier_root) - .expect("nullifier insertion witness invalid"); - - commitment_tree - .insert(commitment) - .expect("commitment tree insert failed"); + // ----------------------------------------------------------------------- + // 3c. Per-compliance-unit tree updates (across all transactions). + // + // Nullifier: `InsertionWitness::apply` proves that `consumed_nullifier` + // is not yet in the indexed nullifier tree (non-membership), + // inserts it, and returns the new nullifier root. + // + // Commitment: `IncrementalMerkleTree::insert` appends `created_commitment` + // to the incremental commitment tree. + // ----------------------------------------------------------------------- + for ((nf, commitment), nf_witness) in nullifiers + .iter() + .zip(commitments.iter()) + .zip(witness.nullifier_witnesses.iter()) + { + nullifier_root = nf_witness + .apply(nf, &nullifier_root) + .expect("nullifier insertion witness invalid"); - nullifier_witness_idx += 1; - } - } + commitment_tree + .insert(*commitment) + .expect("commitment tree insert failed"); } // ----------------------------------------------------------------------- @@ -166,5 +280,7 @@ pub fn main() { old_nullifier_tree_root: witness.old_nullifier_tree_root, new_commitment_root: commitment_tree.root(), new_nullifier_tree_root: nullifier_root, + consumed_resource_app_data, + created_resource_app_data, }); } diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 13fdd9ea..707d36fa 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -91,6 +91,8 @@ mod tests { old_nullifier_tree_root: d(2), new_commitment_root: d(3), new_nullifier_tree_root: d(4), + consumed_resource_app_data: vec![], + created_resource_app_data: vec![], }; let encoded = bincode::serialize(&instance).unwrap(); let decoded: ExecutionProofInstance = bincode::deserialize(&encoded).unwrap(); From 0fd7a2d4f0602b753b822e398e2fa58fae7203ce Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Mon, 30 Mar 2026 14:21:24 +0800 Subject: [PATCH 11/21] add BATCH_AGGREGATION_VK_BYTES and COMPLIANCE_VK_BYTES to ExecutionProofWitness and ExecutionProofInstance --- arm/src/execution_proof.rs | 34 ++++++++-- .../execution_proof/methods/guest/src/main.rs | 64 +++++++++++-------- arm_circuits/execution_proof/src/main.rs | 15 +++++ 3 files changed, 82 insertions(+), 31 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 8ac8ee54..d18994d5 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -6,15 +6,26 @@ //! transaction it checks: //! //! 1. **Nullifier uniqueness** — no two compliance units in the batch consume -//! the same nullifier. It also implies that all commimtments are unique. +//! the same nullifier, checked via sort + adjacent comparison. //! 2. **Delta proof** — the transaction's delta proof is valid. //! 3. **Aggregation proof** — all compliance and logic proofs within the -//! transaction have been aggregated and verified. +//! transaction have been aggregated and verified against +//! [`ExecutionProofWitness::batch_aggregation_vk`] and +//! [`ExecutionProofWitness::compliance_vk`]. For each resource the +//! circuit also asserts that the logic verifier input's `verifying_key` +//! matches the `logic_ref` committed in the corresponding compliance +//! instance, and collects [`ResourceAppData`] for consumed and created +//! resources. //! 4. **Nullifier non-membership + insertion** — each consumed nullifier is //! absent from the indexed nullifier tree ([`InsertionWitness::apply`] //! proves non-membership and returns the updated root atomically). //! 5. **Commitment insertion** — each created commitment is appended to the //! incremental commitment tree. +//! +//! The resulting [`ExecutionProofInstance`] binds the pre- and post-batch +//! tree roots, the per-resource application data, and the verifying keys used +//! during verification, so downstream verifiers can chain proofs and inspect +//! resource payloads without re-running the circuit. use crate::{ incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, AppData, @@ -33,11 +44,13 @@ pub struct ResourceAppData { pub app_data: AppData, } -/// Public outputs of the execution proof, committed to the journal. +/// Public outputs of the execution proof, committed to the RISC0 journal. /// /// Downstream verifiers chain proofs by checking that /// `old_commitment_tree_root` and `old_nullifier_tree_root` of a later proof -/// match the outputs of the preceding one. +/// match the outputs of the preceding one. The committed `batch_aggregation_vk` +/// and `compliance_vk` make the verifying keys that were used during proof +/// verification an explicit part of the instance, binding them to the journal. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ExecutionProofInstance { /// Commitment tree root before the batch was executed. @@ -52,6 +65,10 @@ pub struct ExecutionProofInstance { pub consumed_resource_app_data: Vec, /// Application data for each created resource in this execution batch. pub created_resource_app_data: Vec, + /// Verifying key for the batch aggregation circuit. + pub batch_aggregation_vk: Digest, + /// Verifying key for the compliance circuit. + pub compliance_vk: Digest, } /// Private witness consumed by the execution proof circuit. @@ -62,8 +79,13 @@ pub struct ExecutionProofInstance { /// simultaneously proves non-membership of the consumed nullifier and derives /// the new nullifier root via [`InsertionWitness::apply`]. /// +/// The [`batch_aggregation_vk`] and [`compliance_vk`] are passed in rather +/// than hardcoded, so the circuit can be used with different deployments. +/// /// [`commitment_tree`]: ExecutionProofWitness::commitment_tree /// [`nullifier_witnesses`]: ExecutionProofWitness::nullifier_witnesses +/// [`batch_aggregation_vk`]: ExecutionProofWitness::batch_aggregation_vk +/// [`compliance_vk`]: ExecutionProofWitness::compliance_vk #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { /// The transactions to execute and verify. @@ -79,4 +101,8 @@ pub struct ExecutionProofWitness { /// from the current nullifier tree root and returns the root after /// insertion, threading state forward to the next witness. pub nullifier_witnesses: Vec, + /// Verifying key for the batch aggregation circuit. + pub batch_aggregation_vk: Digest, + /// Verifying key for the compliance circuit. + pub compliance_vk: Digest, } diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index dcab784a..dd8ad6da 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -2,7 +2,6 @@ use anoma_rm_risc0::{ action::Action, action_tree::MerkleTree, compliance::ComplianceInstanceWords, - constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, delta_proof::DeltaProof, execution_proof::{ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, transaction::{Delta, TransactionExt}, @@ -10,12 +9,9 @@ use anoma_rm_risc0::{ }; use risc0_zkvm::guest::env; -/// Converts a 32-byte VK constant to the `risc0_zkvm::sha::Digest` expected by `env::verify`. -fn vk_to_risc0(bytes: &[u8; 32]) -> risc0_zkvm::sha::Digest { - let words: [u32; 8] = arm_core::utils::bytes_to_words(bytes) - .try_into() - .expect("32 bytes always yields 8 words"); - risc0_zkvm::sha::Digest::new(words) +/// Converts an ARM [`Digest`] to the `risc0_zkvm::sha::Digest` expected by `env::verify`. +fn vk_to_risc0(vk: &Digest) -> risc0_zkvm::sha::Digest { + risc0_zkvm::sha::Digest::new(*vk.as_words()) } /// Output of [`collect_action_logic`]: the serialised logic proof data needed @@ -116,10 +112,14 @@ struct TxVerificationData { /// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. /// +/// Combines the compliance instances with the per-action logic data (collected +/// by [`collect_action_logic`]) and `compliance_vk` into the tuple expected by +/// the batch aggregation circuit, then serialises it with `risc0_zkvm::serde`. +/// /// Delegates per-action work to [`collect_action_logic`], which visits each /// action's compliance units and `logic_verifier_inputs` exactly once. -/// The returned [`TxVerificationData`] carries everything needed for tree -/// updates and the final instance, so callers need no further iteration. +/// The returned [`TxVerificationData`] also carries the [`ResourceAppData`] +/// for all resources in the transaction. fn aggregation_instance_words( tx: &anoma_rm_risc0::Transaction, compliance_vk: &Digest, @@ -180,8 +180,8 @@ pub fn main() { // two compliance units *within this batch* from consuming the same // nullifier before any of them reach the tree-update step. // - // Nullifiers and commitments are collected here for reuse in the - // tree-update step, avoiding a second pass over the transactions. + // Nullifiers and commitments are collected here in tx → action → CU + // order for reuse in step 3c, avoiding a second pass over the witness. // ----------------------------------------------------------------------- let mut nullifiers: Vec = Vec::new(); let mut commitments: Vec = Vec::new(); @@ -195,8 +195,8 @@ pub fn main() { } // Sort a copy of the nullifiers and check adjacent pairs for duplicates. - // Sorting with integer comparisons is far cheaper in the zkVM than hashing - // with HashSet (SipHash has no RISC0 accelerator and costs many cycles per call). + // Sorting uses only integer comparisons (Digest is [u32; 8]), which is far + // cheaper in the zkVM than HashSet whose SipHash has no RISC0 accelerator. let mut sorted_nullifiers = nullifiers.clone(); sorted_nullifiers.sort_by_key(|d| *d.as_words()); for window in sorted_nullifiers.windows(2) { @@ -208,10 +208,12 @@ pub fn main() { // ----------------------------------------------------------------------- // 3. Per-transaction verification and state transition. + // + // VKs are taken from the witness rather than hardcoded constants so the + // circuit is not tied to a specific deployment. // ----------------------------------------------------------------------- - let batch_agg_vk_risc0 = vk_to_risc0(&BATCH_AGGREGATION_VK_BYTES); - let compliance_vk_core = - Digest::try_from(COMPLIANCE_VK_BYTES.as_slice()).expect("compliance VK bytes"); + let batch_agg_vk_risc0 = vk_to_risc0(&witness.batch_aggregation_vk); + let compliance_vk = witness.compliance_vk; let mut consumed_resource_app_data: Vec = Vec::new(); let mut created_resource_app_data: Vec = Vec::new(); @@ -231,17 +233,17 @@ pub fn main() { Delta::Witness(_) => panic!("expected delta proof, got witness"), } - // --- 3b. Batch aggregation proof + data collection --- + // --- 3b. Batch aggregation proof --- // - // Confirms that every compliance proof and logic proof inside this - // transaction has been verified by the batch aggregation circuit. - // Nullifiers, commitments, and ResourceAppData are all collected in the - // same pass, so no further iteration over actions is needed. + // Verifies that every compliance proof and logic proof inside this + // transaction was verified by the batch aggregation circuit. + // `aggregation_instance_words` also collects ResourceAppData in the + // same pass over logic_verifier_inputs (see TxVerificationData). assert!( tx.aggregation_proof.is_some(), "transaction is missing an aggregation proof" ); - let tx_data = aggregation_instance_words(tx, &compliance_vk_core); + let tx_data = aggregation_instance_words(tx, &compliance_vk); env::verify(batch_agg_vk_risc0, &tx_data.agg_words) .expect("aggregation proof verification failed"); consumed_resource_app_data.extend(tx_data.consumed_resource_app_data); @@ -251,12 +253,15 @@ pub fn main() { // ----------------------------------------------------------------------- // 3c. Per-compliance-unit tree updates (across all transactions). // - // Nullifier: `InsertionWitness::apply` proves that `consumed_nullifier` - // is not yet in the indexed nullifier tree (non-membership), - // inserts it, and returns the new nullifier root. + // Nullifier: `InsertionWitness::apply` proves non-membership of the + // consumed nullifier in the current indexed nullifier tree, + // inserts it, and returns the updated root. // - // Commitment: `IncrementalMerkleTree::insert` appends `created_commitment` - // to the incremental commitment tree. + // Commitment: `IncrementalMerkleTree::insert` appends the created + // commitment to the incremental commitment tree. + // + // `nullifiers` and `commitments` were pre-collected in step 2, so no + // further iteration over the witness transactions is required here. // ----------------------------------------------------------------------- for ((nf, commitment), nf_witness) in nullifiers .iter() @@ -274,6 +279,9 @@ pub fn main() { // ----------------------------------------------------------------------- // 4. Commit the instance. + // + // The VKs are committed alongside the tree roots and resource app-data + // so verifiers can inspect which circuit versions were used. // ----------------------------------------------------------------------- env::commit(&ExecutionProofInstance { old_commitment_tree_root, @@ -282,5 +290,7 @@ pub fn main() { new_nullifier_tree_root: nullifier_root, consumed_resource_app_data, created_resource_app_data, + batch_aggregation_vk: witness.batch_aggregation_vk, + compliance_vk: witness.compliance_vk, }); } diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 707d36fa..60ad0801 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -51,12 +51,17 @@ pub fn prove( #[cfg(test)] mod tests { use anoma_rm_risc0::{ + constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, Digest, }; + fn vk(bytes: &[u8; 32]) -> Digest { + Digest::try_from(bytes.as_slice()).expect("vk bytes") + } + fn empty_witness() -> ExecutionProofWitness { let nullifier_tree = IndexedMerkleTree::new(); ExecutionProofWitness { @@ -64,6 +69,8 @@ mod tests { commitment_tree: IncrementalMerkleTree::new(3), old_nullifier_tree_root: nullifier_tree.root(), nullifier_witnesses: vec![], + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), } } @@ -93,6 +100,8 @@ mod tests { new_nullifier_tree_root: d(4), consumed_resource_app_data: vec![], created_resource_app_data: vec![], + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; let encoded = bincode::serialize(&instance).unwrap(); let decoded: ExecutionProofInstance = bincode::deserialize(&encoded).unwrap(); @@ -149,6 +158,8 @@ mod tests { commitment_tree, old_nullifier_tree_root: old_nullifier_root, nullifier_witnesses: vec![], + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); @@ -209,6 +220,8 @@ mod tests { commitment_tree, old_nullifier_tree_root: old_nullifier_root, nullifier_witnesses, + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); @@ -280,6 +293,8 @@ mod tests { commitment_tree, old_nullifier_tree_root: old_nullifier_root, nullifier_witnesses, + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); From 0afdfb164880e8f87f88b2516df096371c34921b Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Thu, 2 Apr 2026 00:15:51 +0800 Subject: [PATCH 12/21] fix clippy --- arm/src/indexed_merkle_tree.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs index cc9c705c..98224d78 100644 --- a/arm/src/indexed_merkle_tree.rs +++ b/arm/src/indexed_merkle_tree.rs @@ -495,9 +495,9 @@ mod tests { #[test] fn digest_lt_sentinels() { - assert!(digest_lt(&*MIN_VALUE, &d(1))); - assert!(digest_lt(&d(1), &*MAX_VALUE)); - assert!(!digest_lt(&*MAX_VALUE, &d(1))); + assert!(digest_lt(&MIN_VALUE, &d(1))); + assert!(digest_lt(&d(1), &MAX_VALUE)); + assert!(!digest_lt(&MAX_VALUE, &d(1))); } // ── IndexedLeaf::hash ──────────────────────────────────────────────────── From cf6c9ca51617975236b02126f3b4330ffed6c416 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Thu, 2 Apr 2026 22:13:12 +0800 Subject: [PATCH 13/21] execution proof benchmark --- arm_circuits/Cargo.lock | 201 ++++++++++++++++++ arm_circuits/execution_proof/Cargo.toml | 6 + arm_circuits/execution_proof/benches/prove.rs | 176 +++++++++++++++ 3 files changed, 383 insertions(+) create mode 100644 arm_circuits/execution_proof/benches/prove.rs diff --git a/arm_circuits/Cargo.lock b/arm_circuits/Cargo.lock index 3f178933..0603770d 100644 --- a/arm_circuits/Cargo.lock +++ b/arm_circuits/Cargo.lock @@ -234,6 +234,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anoma-rm-core" version = "1.0.0" @@ -303,6 +309,12 @@ dependencies = [ "serde", ] +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.100" @@ -980,6 +992,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.39" @@ -1025,6 +1043,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1075,6 +1120,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cobs" version = "0.3.0" @@ -1205,6 +1275,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1756,6 +1862,7 @@ dependencies = [ "anoma-rm-risc0", "anoma-rm-risc0-test-app", "bincode", + "criterion", "execution-proof-methods", "risc0-zkvm", ] @@ -2136,6 +2243,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -2591,6 +2709,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.1", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3219,6 +3348,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -3363,6 +3498,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polyval" version = "0.6.2" @@ -4502,6 +4665,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schemars" version = "0.9.0" @@ -5012,6 +5184,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -5408,6 +5590,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5577,6 +5769,15 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.1", +] + [[package]] name = "windows-core" version = "0.62.1" diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml index 3ce0c47d..4caa19e3 100644 --- a/arm_circuits/execution_proof/Cargo.toml +++ b/arm_circuits/execution_proof/Cargo.toml @@ -14,6 +14,12 @@ bincode = "1.3.3" [dev-dependencies] anoma-rm-risc0 = { path = "../../arm", features = ["aggregation", "prove"] } anoma-rm-risc0-test-app = { path = "../../arm_tests/arm_test_app" } +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "prove" +harness = false +required-features = ["prove"] [features] default = [] diff --git a/arm_circuits/execution_proof/benches/prove.rs b/arm_circuits/execution_proof/benches/prove.rs new file mode 100644 index 00000000..ef198dee --- /dev/null +++ b/arm_circuits/execution_proof/benches/prove.rs @@ -0,0 +1,176 @@ +/// Execution proof generation benchmarks. +/// +/// These benchmarks measure wall-clock time of the execution proof prover +/// across different batch sizes (transaction count) and compliance-unit counts. +/// +/// # Running on CPU +/// +/// ```sh +/// cargo bench --features prove -p execution-proof +/// ``` +/// +/// # Running on GPU (CUDA) +/// +/// Build with the `cuda` feature to enable RISC0's GPU prover backend. +/// RISC0 selects CUDA automatically when the feature is compiled in and a +/// compatible GPU is present — no extra environment variables are required. +/// +/// ```sh +/// cargo bench --features prove,cuda -p execution-proof +/// ``` +/// +/// To compare CPU vs GPU, save two baselines: +/// +/// ```sh +/// cargo bench --features prove -p execution-proof -- --save-baseline cpu +/// cargo bench --features prove,cuda -p execution-proof -- --save-baseline gpu +/// cargo bench --features prove,cuda -p execution-proof -- --baseline cpu +/// ``` +/// +/// # Fast / dev-mode iteration +/// +/// Set `RISC0_DEV_MODE=1` to skip actual proof generation and validate only +/// the witness-assembly logic without the multi-minute prover overhead. +/// +/// ```sh +/// RISC0_DEV_MODE=1 cargo bench --features prove -p execution-proof +/// ``` +use anoma_rm_risc0::{ + constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, + execution_proof::ExecutionProofWitness, + incremental_merkle_tree::IncrementalMerkleTree, + indexed_merkle_tree::IndexedMerkleTree, + proving_system::ProofType, + transaction::{Delta, Transaction}, + CoreDeltaWitness, Digest, TransactionExt, +}; +use anoma_rm_risc0_test_app::create_an_action_with_multiple_compliances; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use std::time::Duration; +use execution_proof_methods::EXECUTION_PROOF_GUEST_ELF; +use risc0_zkvm::{default_prover, ExecutorEnv, InnerReceipt, ProverOpts, VerifierContext}; + +fn vk(bytes: &[u8; 32]) -> Digest { + Digest::try_from(bytes.as_slice()).expect("vk bytes") +} + +/// Prove an execution witness. Mirrors the logic in `src/main.rs::prove`. +fn do_prove(witness: &ExecutionProofWitness) -> risc0_zkvm::Receipt { + let mut env_builder = ExecutorEnv::builder(); + + for tx in &witness.transactions { + if let Some(agg_bytes) = &tx.aggregation_proof { + let inner: InnerReceipt = bincode::deserialize(agg_bytes).unwrap(); + env_builder.add_assumption(inner); + } else { + for inner in tx.get_compliance_inner_receipts().unwrap() { + env_builder.add_assumption(inner); + } + for inner in tx.get_logic_inner_receipts().unwrap() { + env_builder.add_assumption(inner); + } + } + } + + let env = env_builder.write(witness).unwrap().build().unwrap(); + + default_prover() + .prove_with_ctx( + env, + &VerifierContext::default(), + EXECUTION_PROOF_GUEST_ELF, + &ProverOpts::succinct(), + ) + .unwrap() + .receipt +} + +/// Build one aggregated transaction: `n_compliances` compliance units, +/// a distinct `nonce` so all nullifiers across a batch remain unique. +fn build_aggregated_tx(n_compliances: usize, nonce: u8) -> Transaction { + let (action, dw) = + create_an_action_with_multiple_compliances(n_compliances, nonce, ProofType::Succinct); + let mut tx = Transaction::create( + vec![action], + Delta::Witness(CoreDeltaWitness(dw.to_bytes())), + ) + .generate_delta_proof() + .unwrap(); + tx.aggregate(ProofType::Succinct).unwrap(); + tx +} + +/// Assemble an `ExecutionProofWitness` from pre-aggregated transactions. +/// A fresh `IndexedMerkleTree` is used so every benchmark iteration starts +/// from the same initial tree state. +fn build_witness(transactions: Vec) -> ExecutionProofWitness { + let mut nullifier_tree = IndexedMerkleTree::new(); + let mut nullifier_witnesses = Vec::new(); + + for tx in &transactions { + for action in &tx.actions { + for cu in action.get_compliance_units() { + let w = nullifier_tree + .insert(cu.instance.consumed_nullifier) + .unwrap(); + nullifier_witnesses.push(w); + } + } + } + + ExecutionProofWitness { + transactions, + commitment_tree: IncrementalMerkleTree::new(3), + old_nullifier_tree_root: IndexedMerkleTree::new().root(), + nullifier_witnesses, + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), + } +} + +/// Vary the number of transactions in a batch (1 compliance unit each). +fn bench_by_tx_count(c: &mut Criterion) { + let mut group = c.benchmark_group("execution_proof/tx_count"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(22000)); + + for n_txs in [1usize, 2] { + // Build all transactions outside the timing loop. + let transactions: Vec = (0..n_txs) + .map(|i| build_aggregated_tx(1, i as u8)) + .collect(); + + group.bench_with_input( + BenchmarkId::from_parameter(n_txs), + &transactions, + |b, txs| { + b.iter_with_setup(|| build_witness(txs.clone()), |witness| do_prove(&witness)); + }, + ); + } + + group.finish(); +} + +/// Vary the number of compliance units per transaction (single transaction). +fn bench_by_compliance_count(c: &mut Criterion) { + let mut group = c.benchmark_group("execution_proof/compliance_count"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(22000)); + + for n_compliances in [1usize, 2, 4] { + let tx = build_aggregated_tx(n_compliances, 0); + + group.bench_with_input(BenchmarkId::from_parameter(n_compliances), &tx, |b, tx| { + b.iter_with_setup( + || build_witness(vec![tx.clone()]), + |witness| do_prove(&witness), + ); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_by_tx_count, bench_by_compliance_count); +criterion_main!(benches); From 8c1ece31a6ca406ac7e8e04b9f5d6ada6db403f0 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Fri, 3 Apr 2026 23:25:39 +0800 Subject: [PATCH 14/21] update execution proof benchmark --- arm_circuits/execution_proof/benches/prove.rs | 12 +++++++----- arm_circuits/execution_proof/src/main.rs | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/arm_circuits/execution_proof/benches/prove.rs b/arm_circuits/execution_proof/benches/prove.rs index ef198dee..eb284693 100644 --- a/arm_circuits/execution_proof/benches/prove.rs +++ b/arm_circuits/execution_proof/benches/prove.rs @@ -46,9 +46,9 @@ use anoma_rm_risc0::{ }; use anoma_rm_risc0_test_app::create_an_action_with_multiple_compliances; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use std::time::Duration; use execution_proof_methods::EXECUTION_PROOF_GUEST_ELF; use risc0_zkvm::{default_prover, ExecutorEnv, InnerReceipt, ProverOpts, VerifierContext}; +use std::time::Duration; fn vk(bytes: &[u8; 32]) -> Digest { Digest::try_from(bytes.as_slice()).expect("vk bytes") @@ -79,7 +79,7 @@ fn do_prove(witness: &ExecutionProofWitness) -> risc0_zkvm::Receipt { env, &VerifierContext::default(), EXECUTION_PROOF_GUEST_ELF, - &ProverOpts::succinct(), + &ProverOpts::groth16(), ) .unwrap() .receipt @@ -132,9 +132,10 @@ fn build_witness(transactions: Vec) -> ExecutionProofWitness { fn bench_by_tx_count(c: &mut Criterion) { let mut group = c.benchmark_group("execution_proof/tx_count"); group.sample_size(10); - group.measurement_time(Duration::from_secs(22000)); + group.warm_up_time(Duration::from_secs(1)); + group.measurement_time(Duration::from_secs(1)); - for n_txs in [1usize, 2] { + for n_txs in [1usize, 2, 4, 8, 16] { // Build all transactions outside the timing loop. let transactions: Vec = (0..n_txs) .map(|i| build_aggregated_tx(1, i as u8)) @@ -156,7 +157,8 @@ fn bench_by_tx_count(c: &mut Criterion) { fn bench_by_compliance_count(c: &mut Criterion) { let mut group = c.benchmark_group("execution_proof/compliance_count"); group.sample_size(10); - group.measurement_time(Duration::from_secs(22000)); + group.warm_up_time(Duration::from_secs(1)); + group.measurement_time(Duration::from_secs(1)); for n_compliances in [1usize, 2, 4] { let tx = build_aggregated_tx(n_compliances, 0); diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 60ad0801..ad59896a 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -253,6 +253,7 @@ mod tests { // Build each transaction with a distinct nonce (0 and 1) so their // compliance units produce different resources and unique nullifiers. + let t = std::time::Instant::now(); let (action1, dw1) = create_an_action_with_multiple_compliances(1, 0, ProofType::Succinct); let mut tx1 = Transaction::create( vec![action1], @@ -261,6 +262,7 @@ mod tests { .generate_delta_proof() .unwrap(); tx1.aggregate(ProofType::Succinct).unwrap(); + println!("tx1 generation time: {:?}", t.elapsed()); let (action2, dw2) = create_an_action_with_multiple_compliances(1, 1, ProofType::Succinct); let mut tx2 = Transaction::create( @@ -297,7 +299,9 @@ mod tests { compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; + let t0 = std::time::Instant::now(); let receipt = super::prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); + println!("proof generation time: {:?}", t0.elapsed()); let instance: ExecutionProofInstance = receipt.journal.decode().unwrap(); assert_eq!(instance.old_nullifier_tree_root, old_nullifier_root); From af712d40e7a622e0e2acc4426d558ab874837cf6 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 7 Apr 2026 15:32:15 +0800 Subject: [PATCH 15/21] add prove entrypoint to execution-proof binary for pprof profiling --- arm_circuits/execution_proof/Cargo.toml | 12 +++--- arm_circuits/execution_proof/src/main.rs | 51 +++++++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml index 4caa19e3..8e869e3c 100644 --- a/arm_circuits/execution_proof/Cargo.toml +++ b/arm_circuits/execution_proof/Cargo.toml @@ -10,6 +10,13 @@ anoma-rm-risc0 = { path = "../../arm", features = [ "transaction", ], default-features = false } bincode = "1.3.3" +anoma-rm-risc0-test-app = { path = "../../arm_tests/arm_test_app", optional = true } + +[features] +default = [] +cuda = ["risc0-zkvm/cuda"] +prove = ["risc0-zkvm/prove", "anoma-rm-risc0/aggregation", "anoma-rm-risc0/prove", "dep:anoma-rm-risc0-test-app"] +bonsai = ["risc0-zkvm/bonsai"] [dev-dependencies] anoma-rm-risc0 = { path = "../../arm", features = ["aggregation", "prove"] } @@ -21,8 +28,3 @@ name = "prove" harness = false required-features = ["prove"] -[features] -default = [] -cuda = ["risc0-zkvm/cuda"] -prove = ["risc0-zkvm/prove"] -bonsai = ["risc0-zkvm/bonsai"] diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index ad59896a..36053567 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -2,7 +2,56 @@ use anoma_rm_risc0::execution_proof::ExecutionProofWitness; pub fn main() { - // Do nothing; this is just a placeholder main function. + #[cfg(feature = "prove")] + { + use anoma_rm_risc0::{ + constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, + incremental_merkle_tree::IncrementalMerkleTree, + indexed_merkle_tree::IndexedMerkleTree, + proving_system::ProofType, + transaction::{Delta, Transaction}, + CoreDeltaWitness, Digest, TransactionExt, + }; + use anoma_rm_risc0_test_app::create_an_action_with_multiple_compliances; + + fn vk(bytes: &[u8; 32]) -> Digest { + Digest::try_from(bytes.as_slice()).expect("vk bytes") + } + + let (action, dw) = create_an_action_with_multiple_compliances(1, 0, ProofType::Succinct); + let mut tx = Transaction::create( + vec![action], + Delta::Witness(CoreDeltaWitness(dw.to_bytes())), + ) + .generate_delta_proof() + .unwrap(); + tx.aggregate(ProofType::Succinct).unwrap(); + + let mut nullifier_tree = IndexedMerkleTree::new(); + let mut nullifier_witnesses = Vec::new(); + for action in &tx.actions { + for cu in action.get_compliance_units() { + nullifier_witnesses.push( + nullifier_tree + .insert(cu.instance.consumed_nullifier) + .unwrap(), + ); + } + } + + let witness = ExecutionProofWitness { + transactions: vec![tx], + commitment_tree: IncrementalMerkleTree::new(3), + old_nullifier_tree_root: IndexedMerkleTree::new().root(), + nullifier_witnesses, + batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), + compliance_vk: vk(&COMPLIANCE_VK_BYTES), + }; + + let t = std::time::Instant::now(); + prove(&witness, risc0_zkvm::ProverOpts::succinct()).unwrap(); + println!("proof generation time: {:?}", t.elapsed()); + } } /// Builds an ExecutorEnv with all inner receipts from the witness added as From ea8533e90fc49587a11e69ac1b4207a201bf6363 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 7 Apr 2026 19:24:13 +0800 Subject: [PATCH 16/21] replace Vec with Vec in ExecutionProofWitness --- arm/src/execution_proof.rs | 114 +++++++++++++++++- arm/src/lib.rs | 1 + arm_circuits/execution_proof/Cargo.toml | 8 +- arm_circuits/execution_proof/benches/prove.rs | 22 ++-- .../execution_proof/methods/guest/src/main.rs | 95 +++++++++------ arm_circuits/execution_proof/src/main.rs | 53 +++++--- 6 files changed, 216 insertions(+), 77 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index d18994d5..9300ba21 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -29,7 +29,7 @@ use crate::{ incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, AppData, - Digest, Transaction, + ComplianceInstance, Delta, Digest, Transaction, }; use serde::{Deserialize, Serialize}; @@ -71,6 +71,114 @@ pub struct ExecutionProofInstance { pub compliance_vk: Digest, } +/// Compact logic verifier input for the execution proof circuit. +/// +/// A stripped-down version of [`LogicVerifierInputs`] carrying only the three +/// fields the guest actually reads. The `proof` bytes (potentially hundreds +/// of kilobytes) are deliberately omitted so they are never written to the +/// zkVM's input channel. +/// +/// [`LogicVerifierInputs`]: crate::LogicVerifierInputs +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct LogicVerifierInfo { + /// The resource tag (nullifier for consumed, commitment for created). + pub tag: Digest, + /// The verifying key of the resource's logic proof circuit. + pub verifying_key: Digest, + /// The application data payload for this resource. + pub app_data: AppData, +} + +/// Compact action data for the execution proof circuit. +/// +/// Contains only the data the guest needs: the compliance instances (for delta +/// computation and aggregation instance construction) and the stripped logic +/// verifier inputs (for app-data collection and logic-ref consistency checks). +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ActionInfo { + /// Compliance instances for this action, one per compliance unit. + pub compliance_instances: Vec, + /// Logic verifier inputs stripped of their proof bytes. + pub logic_verifier_inputs: Vec, +} + +/// Compact transaction data for the execution proof circuit. +/// +/// Replaces [`Transaction`] in [`ExecutionProofWitness`] so that only the +/// data the guest actually needs is serialised over the `env::write` channel. +/// The aggregation proof receipt (potentially megabytes as a full STARK +/// receipt) is excluded from serialisation via `#[serde(skip)]` and kept +/// host-side only, where it is registered with `add_assumption`. +/// +/// [`Transaction`]: crate::Transaction +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TxInfo { + /// Per-action compliance and logic data for this transaction. + pub actions: Vec, + /// The 65-byte ECDSA delta proof bytes. + pub delta_proof: Vec, + /// The serialised aggregation proof `InnerReceipt` (host-only). + /// + /// This field is skipped during serialisation so it is never written to + /// the guest. The host extracts it, deserialises it as an + /// `InnerReceipt`, and registers it with `env_builder.add_assumption`. + #[serde(skip)] + pub aggregation_proof: Vec, +} + +impl TxInfo { + /// Converts a fully-proved [`Transaction`] into a [`TxInfo`]. + /// + /// Strips every field not needed by the guest (individual compliance and + /// logic proof bytes, delta witness) while preserving `aggregation_proof` + /// for the host to register as an assumption. + /// + /// Returns an error if `tx.delta_proof` is still a witness, or if the + /// transaction is missing its aggregation proof. + pub fn from_transaction(tx: &Transaction) -> Result { + let actions = tx + .actions + .iter() + .map(|action| { + let compliance_instances = action + .compliance_units + .iter() + .map(|cu| cu.instance.clone()) + .collect(); + let logic_verifier_inputs = action + .logic_verifier_inputs + .iter() + .map(|lvi| LogicVerifierInfo { + tag: lvi.tag, + verifying_key: lvi.verifying_key, + app_data: lvi.app_data.clone(), + }) + .collect(); + ActionInfo { + compliance_instances, + logic_verifier_inputs, + } + }) + .collect(); + + let delta_proof = match &tx.delta_proof { + Delta::Proof(proof) => proof.0.to_vec(), + Delta::Witness(_) => return Err(crate::ArmError::ExpectedDeltaProof), + }; + + let aggregation_proof = tx + .aggregation_proof + .clone() + .ok_or(crate::ArmError::MissingField("aggregation_proof"))?; + + Ok(TxInfo { + actions, + delta_proof, + aggregation_proof, + }) + } +} + /// Private witness consumed by the execution proof circuit. /// /// Commitment updates are driven by [`commitment_tree`], whose state is @@ -88,8 +196,8 @@ pub struct ExecutionProofInstance { /// [`compliance_vk`]: ExecutionProofWitness::compliance_vk #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { - /// The transactions to execute and verify. - pub transactions: Vec, + /// The transactions to execute and verify, in compact [`TxInfo`] form. + pub transactions: Vec, /// Incremental commitment tree state before the batch. pub commitment_tree: IncrementalMerkleTree, /// Indexed nullifier tree root before the batch. diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 94b2fd99..115306ca 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -53,6 +53,7 @@ pub use crate::action::ActionExt; pub use crate::compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt}; #[cfg(feature = "transaction")] pub use crate::compliance_unit::ComplianceUnitExt; +pub use crate::execution_proof::{ActionInfo, LogicVerifierInfo, TxInfo}; #[cfg(feature = "transaction")] pub use crate::logic_proof::LogicVerifierInputsExt; pub use crate::merkle_path::MerklePathExt; diff --git a/arm_circuits/execution_proof/Cargo.toml b/arm_circuits/execution_proof/Cargo.toml index 8e869e3c..42f98f9f 100644 --- a/arm_circuits/execution_proof/Cargo.toml +++ b/arm_circuits/execution_proof/Cargo.toml @@ -15,7 +15,12 @@ anoma-rm-risc0-test-app = { path = "../../arm_tests/arm_test_app", optional = tr [features] default = [] cuda = ["risc0-zkvm/cuda"] -prove = ["risc0-zkvm/prove", "anoma-rm-risc0/aggregation", "anoma-rm-risc0/prove", "dep:anoma-rm-risc0-test-app"] +prove = [ + "risc0-zkvm/prove", + "anoma-rm-risc0/aggregation", + "anoma-rm-risc0/prove", + "dep:anoma-rm-risc0-test-app", +] bonsai = ["risc0-zkvm/bonsai"] [dev-dependencies] @@ -27,4 +32,3 @@ criterion = { version = "0.5", features = ["html_reports"] } name = "prove" harness = false required-features = ["prove"] - diff --git a/arm_circuits/execution_proof/benches/prove.rs b/arm_circuits/execution_proof/benches/prove.rs index eb284693..14332ba1 100644 --- a/arm_circuits/execution_proof/benches/prove.rs +++ b/arm_circuits/execution_proof/benches/prove.rs @@ -37,7 +37,7 @@ /// ``` use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, - execution_proof::ExecutionProofWitness, + execution_proof::{ExecutionProofWitness, TxInfo}, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, proving_system::ProofType, @@ -59,17 +59,8 @@ fn do_prove(witness: &ExecutionProofWitness) -> risc0_zkvm::Receipt { let mut env_builder = ExecutorEnv::builder(); for tx in &witness.transactions { - if let Some(agg_bytes) = &tx.aggregation_proof { - let inner: InnerReceipt = bincode::deserialize(agg_bytes).unwrap(); - env_builder.add_assumption(inner); - } else { - for inner in tx.get_compliance_inner_receipts().unwrap() { - env_builder.add_assumption(inner); - } - for inner in tx.get_logic_inner_receipts().unwrap() { - env_builder.add_assumption(inner); - } - } + let inner: InnerReceipt = bincode::deserialize(&tx.aggregation_proof).unwrap(); + env_builder.add_assumption(inner); } let env = env_builder.write(witness).unwrap().build().unwrap(); @@ -118,8 +109,13 @@ fn build_witness(transactions: Vec) -> ExecutionProofWitness { } } + let tx_infos: Vec = transactions + .iter() + .map(|tx| TxInfo::from_transaction(tx).expect("tx to tx_info")) + .collect(); + ExecutionProofWitness { - transactions, + transactions: tx_infos, commitment_tree: IncrementalMerkleTree::new(3), old_nullifier_tree_root: IndexedMerkleTree::new().root(), nullifier_witnesses, diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index dd8ad6da..f17509f4 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -1,10 +1,8 @@ use anoma_rm_risc0::{ - action::Action, action_tree::MerkleTree, - compliance::ComplianceInstanceWords, - delta_proof::DeltaProof, - execution_proof::{ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, - transaction::{Delta, TransactionExt}, + compliance::{ComplianceInstanceExt, ComplianceInstanceWords}, + delta_proof::{DeltaInstance, DeltaProof}, + execution_proof::{ActionInfo, ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, Digest, LogicInstance, }; use risc0_zkvm::guest::env; @@ -27,23 +25,23 @@ struct ActionLogicData { created_resource_app_data: Vec, } -/// Processes a single action in one pass over its compliance units: +/// Processes a single action in one pass over its compliance instances: /// /// - Builds the ordered tag / logic-ref lists for the action tree. /// - Asserts that each input's `verifying_key` matches the corresponding /// `logic_ref` committed inside the compliance instance. /// - Serialises each [`LogicInstance`] for the aggregation proof. /// - Splits [`ResourceAppData`] into consumed and created buckets. -fn collect_action_logic(action: &Action) -> ActionLogicData { +fn collect_action_logic(action: &ActionInfo) -> ActionLogicData { let mut tags = Vec::new(); let mut logic_refs = Vec::new(); - for cu in action.get_compliance_units() { + for ci in &action.compliance_instances { // Ordered as [consumed, created] per CU to match proof construction. - tags.push(cu.instance.consumed_nullifier); - logic_refs.push(cu.instance.consumed_logic_ref); - tags.push(cu.instance.created_commitment); - logic_refs.push(cu.instance.created_logic_ref); + tags.push(ci.consumed_nullifier); + logic_refs.push(ci.consumed_logic_ref); + tags.push(ci.created_commitment); + logic_refs.push(ci.created_logic_ref); } let root = MerkleTree::from(tags.clone()) @@ -59,7 +57,7 @@ fn collect_action_logic(action: &Action) -> ActionLogicData { let is_consumed = index % 2 == 0; let input = action - .get_logic_verifier_inputs() + .logic_verifier_inputs .iter() .find(|i| i.tag == *tag) .expect("logic verifier input not found for tag"); @@ -112,23 +110,26 @@ struct TxVerificationData { /// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. /// -/// Combines the compliance instances with the per-action logic data (collected -/// by [`collect_action_logic`]) and `compliance_vk` into the tuple expected by -/// the batch aggregation circuit, then serialises it with `risc0_zkvm::serde`. +/// Combines the compliance instances (derived directly from [`ActionInfo`]) +/// with the per-action logic data (collected by [`collect_action_logic`]) and +/// `compliance_vk` into the tuple expected by the batch aggregation circuit, +/// then serialises it with `risc0_zkvm::serde`. /// /// Delegates per-action work to [`collect_action_logic`], which visits each -/// action's compliance units and `logic_verifier_inputs` exactly once. +/// action's compliance instances and logic verifier inputs exactly once. /// The returned [`TxVerificationData`] also carries the [`ResourceAppData`] /// for all resources in the transaction. fn aggregation_instance_words( - tx: &anoma_rm_risc0::Transaction, + tx: &anoma_rm_risc0::execution_proof::TxInfo, compliance_vk: &Digest, ) -> TxVerificationData { + // Build compliance instance words directly from the ActionInfo structs — + // no journal byte parsing needed. let compliance_instances_u32: Vec = tx - .get_compliance_instances() - .expect("compliance instances") + .actions .iter() - .map(|b| ComplianceInstanceWords::from_bytes(b).expect("compliance instance words")) + .flat_map(|a| a.compliance_instances.iter()) + .map(ComplianceInstanceWords::from) .collect(); let mut lp_vks = Vec::new(); @@ -187,9 +188,9 @@ pub fn main() { let mut commitments: Vec = Vec::new(); for tx in &witness.transactions { for action in &tx.actions { - for cu in action.get_compliance_units() { - nullifiers.push(cu.instance.consumed_nullifier); - commitments.push(cu.instance.created_commitment); + for ci in &action.compliance_instances { + nullifiers.push(ci.consumed_nullifier); + commitments.push(ci.created_commitment); } } } @@ -221,17 +222,32 @@ pub fn main() { for tx in &witness.transactions { // --- 3a. Delta proof --- // - // Verifies that the net value change (Σ created − Σ consumed) across - // all compliance units in the transaction is zero. - let msg = tx.get_delta_msg(); - let delta_instance = tx.delta().expect("delta instance"); - match &tx.delta_proof { - Delta::Proof(core_proof) => { - let proof = DeltaProof::from_bytes(&core_proof.0).expect("deserialize delta proof"); - DeltaProof::verify(&msg, &proof, delta_instance).expect("delta proof invalid"); - } - Delta::Witness(_) => panic!("expected delta proof, got witness"), - } + // Compute the delta message (concat of nullifier+commitment bytes for + // each compliance unit) and the delta instance (sum of delta EC points + // from each compliance instance) directly from the ActionInfo data. + // No Transaction object or journal parsing is needed. + let msg: Vec = tx + .actions + .iter() + .flat_map(|a| a.compliance_instances.iter()) + .flat_map(|ci| { + let mut bytes = Vec::with_capacity(64); + bytes.extend_from_slice(ci.consumed_nullifier.as_bytes()); + bytes.extend_from_slice(ci.created_commitment.as_bytes()); + bytes + }) + .collect(); + + let delta_points: Vec<_> = tx + .actions + .iter() + .flat_map(|a| a.compliance_instances.iter()) + .map(|ci| ci.delta_projective().expect("delta projective")) + .collect(); + let delta_instance = DeltaInstance::from_deltas(&delta_points).expect("delta instance"); + + let proof = DeltaProof::from_bytes(&tx.delta_proof).expect("deserialize delta proof"); + DeltaProof::verify(&msg, &proof, delta_instance).expect("delta proof invalid"); // --- 3b. Batch aggregation proof --- // @@ -239,10 +255,11 @@ pub fn main() { // transaction was verified by the batch aggregation circuit. // `aggregation_instance_words` also collects ResourceAppData in the // same pass over logic_verifier_inputs (see TxVerificationData). - assert!( - tx.aggregation_proof.is_some(), - "transaction is missing an aggregation proof" - ); + // + // Note: `aggregation_proof` is host-only (`#[serde(skip)]`) so it is + // always empty in the guest; the corresponding InnerReceipt was + // registered as an assumption by the host and is resolved here by + // `env::verify`. let tx_data = aggregation_instance_words(tx, &compliance_vk); env::verify(batch_agg_vk_risc0, &tx_data.agg_words) .expect("aggregation proof verification failed"); diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 36053567..53cc166a 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -6,6 +6,7 @@ pub fn main() { { use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, + execution_proof::TxInfo, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, proving_system::ProofType, @@ -39,8 +40,9 @@ pub fn main() { } } + let tx_info = TxInfo::from_transaction(&tx).unwrap(); let witness = ExecutionProofWitness { - transactions: vec![tx], + transactions: vec![tx_info], commitment_tree: IncrementalMerkleTree::new(3), old_nullifier_tree_root: IndexedMerkleTree::new().root(), nullifier_witnesses, @@ -54,33 +56,21 @@ pub fn main() { } } -/// Builds an ExecutorEnv with all inner receipts from the witness added as -/// assumptions, then proves the execution circuit. +/// Deserialises each transaction's aggregation receipt, registers it as an +/// assumption, writes the witness to the guest, and proves the execution circuit. #[cfg(feature = "prove")] pub fn prove( witness: &ExecutionProofWitness, proof_type: risc0_zkvm::ProverOpts, ) -> Result> { - use anoma_rm_risc0::TransactionExt; use execution_proof_methods::EXECUTION_PROOF_GUEST_ELF; use risc0_zkvm::{default_prover, ExecutorEnv, InnerReceipt, VerifierContext}; let mut env_builder = ExecutorEnv::builder(); for tx in &witness.transactions { - if let Some(agg_bytes) = &tx.aggregation_proof { - // Add the aggregation inner receipt as an assumption. - let inner: InnerReceipt = bincode::deserialize(agg_bytes)?; - env_builder.add_assumption(inner); - } else { - // Add individual compliance and logic inner receipts as assumptions. - for inner in tx.get_compliance_inner_receipts()? { - env_builder.add_assumption(inner); - } - for inner in tx.get_logic_inner_receipts()? { - env_builder.add_assumption(inner); - } - } + let inner: InnerReceipt = bincode::deserialize(&tx.aggregation_proof)?; + env_builder.add_assumption(inner); } let env = env_builder.write(witness)?.build()?; @@ -101,7 +91,7 @@ pub fn prove( mod tests { use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, - execution_proof::{ExecutionProofInstance, ExecutionProofWitness}, + execution_proof::{ExecutionProofInstance, ExecutionProofWitness, TxInfo}, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, Digest, @@ -139,6 +129,23 @@ mod tests { ); } + #[test] + fn tx_info_serde_roundtrip() { + // aggregation_proof is #[serde(skip)] so it is stripped from both + // bincode and risc0 serde output. The host reads it directly from + // the in-memory TxInfo before calling env_builder.write(), so it + // never needs to cross any serialisation boundary. + let tx_info = TxInfo { + actions: vec![], + delta_proof: vec![0u8; 65], + aggregation_proof: vec![1, 2, 3], + }; + let encoded = bincode::serialize(&tx_info).unwrap(); + let decoded: TxInfo = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded.delta_proof, tx_info.delta_proof); + assert_eq!(decoded.aggregation_proof, Vec::::new()); + } + #[test] fn instance_serde_roundtrip() { let d = |v: u32| Digest::new([v, 0, 0, 0, 0, 0, 0, 0]); @@ -264,8 +271,9 @@ mod tests { let old_nullifier_root = IndexedMerkleTree::new().root(); let old_commitment_root = commitment_tree.root(); + let tx_info = TxInfo::from_transaction(&tx).unwrap(); let witness = ExecutionProofWitness { - transactions: vec![tx], + transactions: vec![tx_info], commitment_tree, old_nullifier_tree_root: old_nullifier_root, nullifier_witnesses, @@ -339,8 +347,12 @@ mod tests { let commitment_tree = IncrementalMerkleTree::new(3); let old_nullifier_root = IndexedMerkleTree::new().root(); + let tx_infos: Vec = [&tx1, &tx2] + .iter() + .map(|tx| TxInfo::from_transaction(tx).unwrap()) + .collect(); let witness = ExecutionProofWitness { - transactions: vec![tx1, tx2], + transactions: tx_infos, commitment_tree, old_nullifier_tree_root: old_nullifier_root, nullifier_witnesses, @@ -361,6 +373,7 @@ mod tests { // Updates the ELF binary and prints the image ID. // Run with: cargo test --features prove print_execution_proof_elf_id -- --nocapture +#[ignore] #[test] fn print_execution_proof_elf_id() { use execution_proof_methods::{EXECUTION_PROOF_GUEST_ELF, EXECUTION_PROOF_GUEST_ID}; From d81c6f117819d483d8d35efe837f26db9a4ee4f4 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Sun, 12 Apr 2026 23:38:38 +0800 Subject: [PATCH 17/21] refactor: rename Info suffixes to Input in execution proof types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TxInfo → TxInput, ActionInfo → ActionInput, LogicVerifierInfo → LogicVerifierInput. --- arm/src/execution_proof.rs | 24 +++++++++---------- arm/src/lib.rs | 2 +- arm_circuits/execution_proof/benches/prove.rs | 6 ++--- .../execution_proof/methods/guest/src/main.rs | 12 +++++----- arm_circuits/execution_proof/src/main.rs | 18 +++++++------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 9300ba21..cee19776 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -80,7 +80,7 @@ pub struct ExecutionProofInstance { /// /// [`LogicVerifierInputs`]: crate::LogicVerifierInputs #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct LogicVerifierInfo { +pub struct LogicVerifierInput { /// The resource tag (nullifier for consumed, commitment for created). pub tag: Digest, /// The verifying key of the resource's logic proof circuit. @@ -95,11 +95,11 @@ pub struct LogicVerifierInfo { /// computation and aggregation instance construction) and the stripped logic /// verifier inputs (for app-data collection and logic-ref consistency checks). #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ActionInfo { +pub struct ActionInput { /// Compliance instances for this action, one per compliance unit. pub compliance_instances: Vec, /// Logic verifier inputs stripped of their proof bytes. - pub logic_verifier_inputs: Vec, + pub logic_verifier_inputs: Vec, } /// Compact transaction data for the execution proof circuit. @@ -112,9 +112,9 @@ pub struct ActionInfo { /// /// [`Transaction`]: crate::Transaction #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TxInfo { +pub struct TxInput { /// Per-action compliance and logic data for this transaction. - pub actions: Vec, + pub actions: Vec, /// The 65-byte ECDSA delta proof bytes. pub delta_proof: Vec, /// The serialised aggregation proof `InnerReceipt` (host-only). @@ -126,8 +126,8 @@ pub struct TxInfo { pub aggregation_proof: Vec, } -impl TxInfo { - /// Converts a fully-proved [`Transaction`] into a [`TxInfo`]. +impl TxInput { + /// Converts a fully-proved [`Transaction`] into a [`TxInput`]. /// /// Strips every field not needed by the guest (individual compliance and /// logic proof bytes, delta witness) while preserving `aggregation_proof` @@ -148,13 +148,13 @@ impl TxInfo { let logic_verifier_inputs = action .logic_verifier_inputs .iter() - .map(|lvi| LogicVerifierInfo { + .map(|lvi| LogicVerifierInput { tag: lvi.tag, verifying_key: lvi.verifying_key, app_data: lvi.app_data.clone(), }) .collect(); - ActionInfo { + ActionInput { compliance_instances, logic_verifier_inputs, } @@ -171,7 +171,7 @@ impl TxInfo { .clone() .ok_or(crate::ArmError::MissingField("aggregation_proof"))?; - Ok(TxInfo { + Ok(TxInput { actions, delta_proof, aggregation_proof, @@ -196,8 +196,8 @@ impl TxInfo { /// [`compliance_vk`]: ExecutionProofWitness::compliance_vk #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { - /// The transactions to execute and verify, in compact [`TxInfo`] form. - pub transactions: Vec, + /// The transactions to execute and verify, in compact [`TxInput`] form. + pub transactions: Vec, /// Incremental commitment tree state before the batch. pub commitment_tree: IncrementalMerkleTree, /// Indexed nullifier tree root before the batch. diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 115306ca..28fc63cd 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -53,7 +53,7 @@ pub use crate::action::ActionExt; pub use crate::compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt}; #[cfg(feature = "transaction")] pub use crate::compliance_unit::ComplianceUnitExt; -pub use crate::execution_proof::{ActionInfo, LogicVerifierInfo, TxInfo}; +pub use crate::execution_proof::{ActionInput, LogicVerifierInput, TxInput}; #[cfg(feature = "transaction")] pub use crate::logic_proof::LogicVerifierInputsExt; pub use crate::merkle_path::MerklePathExt; diff --git a/arm_circuits/execution_proof/benches/prove.rs b/arm_circuits/execution_proof/benches/prove.rs index 14332ba1..0fa62f44 100644 --- a/arm_circuits/execution_proof/benches/prove.rs +++ b/arm_circuits/execution_proof/benches/prove.rs @@ -37,7 +37,7 @@ /// ``` use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, - execution_proof::{ExecutionProofWitness, TxInfo}, + execution_proof::{ExecutionProofWitness, TxInput}, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, proving_system::ProofType, @@ -109,9 +109,9 @@ fn build_witness(transactions: Vec) -> ExecutionProofWitness { } } - let tx_infos: Vec = transactions + let tx_infos: Vec = transactions .iter() - .map(|tx| TxInfo::from_transaction(tx).expect("tx to tx_info")) + .map(|tx| TxInput::from_transaction(tx).expect("tx to tx_info")) .collect(); ExecutionProofWitness { diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index f17509f4..d9318f63 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -2,7 +2,7 @@ use anoma_rm_risc0::{ action_tree::MerkleTree, compliance::{ComplianceInstanceExt, ComplianceInstanceWords}, delta_proof::{DeltaInstance, DeltaProof}, - execution_proof::{ActionInfo, ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, + execution_proof::{ActionInput, ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, Digest, LogicInstance, }; use risc0_zkvm::guest::env; @@ -32,7 +32,7 @@ struct ActionLogicData { /// `logic_ref` committed inside the compliance instance. /// - Serialises each [`LogicInstance`] for the aggregation proof. /// - Splits [`ResourceAppData`] into consumed and created buckets. -fn collect_action_logic(action: &ActionInfo) -> ActionLogicData { +fn collect_action_logic(action: &ActionInput) -> ActionLogicData { let mut tags = Vec::new(); let mut logic_refs = Vec::new(); @@ -110,7 +110,7 @@ struct TxVerificationData { /// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. /// -/// Combines the compliance instances (derived directly from [`ActionInfo`]) +/// Combines the compliance instances (derived directly from [`ActionInput`]) /// with the per-action logic data (collected by [`collect_action_logic`]) and /// `compliance_vk` into the tuple expected by the batch aggregation circuit, /// then serialises it with `risc0_zkvm::serde`. @@ -120,10 +120,10 @@ struct TxVerificationData { /// The returned [`TxVerificationData`] also carries the [`ResourceAppData`] /// for all resources in the transaction. fn aggregation_instance_words( - tx: &anoma_rm_risc0::execution_proof::TxInfo, + tx: &anoma_rm_risc0::execution_proof::TxInput, compliance_vk: &Digest, ) -> TxVerificationData { - // Build compliance instance words directly from the ActionInfo structs — + // Build compliance instance words directly from the ActionInput structs — // no journal byte parsing needed. let compliance_instances_u32: Vec = tx .actions @@ -224,7 +224,7 @@ pub fn main() { // // Compute the delta message (concat of nullifier+commitment bytes for // each compliance unit) and the delta instance (sum of delta EC points - // from each compliance instance) directly from the ActionInfo data. + // from each compliance instance) directly from the ActionInput data. // No Transaction object or journal parsing is needed. let msg: Vec = tx .actions diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 53cc166a..170d5a3c 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -6,7 +6,7 @@ pub fn main() { { use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, - execution_proof::TxInfo, + execution_proof::TxInput, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, proving_system::ProofType, @@ -40,7 +40,7 @@ pub fn main() { } } - let tx_info = TxInfo::from_transaction(&tx).unwrap(); + let tx_info = TxInput::from_transaction(&tx).unwrap(); let witness = ExecutionProofWitness { transactions: vec![tx_info], commitment_tree: IncrementalMerkleTree::new(3), @@ -91,7 +91,7 @@ pub fn prove( mod tests { use anoma_rm_risc0::{ constants::{BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES}, - execution_proof::{ExecutionProofInstance, ExecutionProofWitness, TxInfo}, + execution_proof::{ExecutionProofInstance, ExecutionProofWitness, TxInput}, incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::IndexedMerkleTree, Digest, @@ -133,15 +133,15 @@ mod tests { fn tx_info_serde_roundtrip() { // aggregation_proof is #[serde(skip)] so it is stripped from both // bincode and risc0 serde output. The host reads it directly from - // the in-memory TxInfo before calling env_builder.write(), so it + // the in-memory TxInput before calling env_builder.write(), so it // never needs to cross any serialisation boundary. - let tx_info = TxInfo { + let tx_info = TxInput { actions: vec![], delta_proof: vec![0u8; 65], aggregation_proof: vec![1, 2, 3], }; let encoded = bincode::serialize(&tx_info).unwrap(); - let decoded: TxInfo = bincode::deserialize(&encoded).unwrap(); + let decoded: TxInput = bincode::deserialize(&encoded).unwrap(); assert_eq!(decoded.delta_proof, tx_info.delta_proof); assert_eq!(decoded.aggregation_proof, Vec::::new()); } @@ -271,7 +271,7 @@ mod tests { let old_nullifier_root = IndexedMerkleTree::new().root(); let old_commitment_root = commitment_tree.root(); - let tx_info = TxInfo::from_transaction(&tx).unwrap(); + let tx_info = TxInput::from_transaction(&tx).unwrap(); let witness = ExecutionProofWitness { transactions: vec![tx_info], commitment_tree, @@ -347,9 +347,9 @@ mod tests { let commitment_tree = IncrementalMerkleTree::new(3); let old_nullifier_root = IndexedMerkleTree::new().root(); - let tx_infos: Vec = [&tx1, &tx2] + let tx_infos: Vec = [&tx1, &tx2] .iter() - .map(|tx| TxInfo::from_transaction(tx).unwrap()) + .map(|tx| TxInput::from_transaction(tx).unwrap()) .collect(); let witness = ExecutionProofWitness { transactions: tx_infos, From fd93358105714a586f3121b46c6c31c0978fc2e8 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Sun, 12 Apr 2026 23:51:40 +0800 Subject: [PATCH 18/21] efactor: restructure ExecutionProofInstance app-data by action and transaction --- arm/src/execution_proof.rs | 56 +++++++++++++++---- arm/src/lib.rs | 2 +- .../execution_proof/methods/guest/src/main.rs | 36 ++++++------ arm_circuits/execution_proof/src/main.rs | 3 +- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index cee19776..11d4f534 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -15,7 +15,7 @@ //! circuit also asserts that the logic verifier input's `verifying_key` //! matches the `logic_ref` committed in the corresponding compliance //! instance, and collects [`ResourceAppData`] for consumed and created -//! resources. +//! resources, organised per-action as [`ActionInfo`]. //! 4. **Nullifier non-membership + insertion** — each consumed nullifier is //! absent from the indexed nullifier tree ([`InsertionWitness::apply`] //! proves non-membership and returns the updated root atomically). @@ -23,9 +23,24 @@ //! incremental commitment tree. //! //! The resulting [`ExecutionProofInstance`] binds the pre- and post-batch -//! tree roots, the per-resource application data, and the verifying keys used -//! during verification, so downstream verifiers can chain proofs and inspect -//! resource payloads without re-running the circuit. +//! tree roots, the per-transaction [`TxInfo`] (carrying per-action app-data +//! and action tree roots), and the verifying keys used during verification, +//! so downstream verifiers can chain proofs and inspect resource payloads +//! without re-running the circuit. +//! +//! ## Type hierarchy +//! +//! **Inputs** (witness side, written to the zkVM): +//! - [`ExecutionProofWitness`] — full private witness +//! - [`TxInput`] — compact transaction data (proof bytes stripped) +//! - [`ActionInput`] — compliance instances + stripped logic verifier inputs +//! - [`LogicVerifierInput`] — tag, verifying key, and app data for one resource +//! +//! **Outputs** (instance side, committed to the journal): +//! - [`ExecutionProofInstance`] — public outputs of the circuit +//! - [`TxInfo`] — per-transaction structured output +//! - [`ActionInfo`] — action tree root + app data per action +//! - [`ResourceAppData`] — tag, logic vk, and app data for one resource use crate::{ incremental_merkle_tree::IncrementalMerkleTree, indexed_merkle_tree::InsertionWitness, AppData, @@ -44,13 +59,36 @@ pub struct ResourceAppData { pub app_data: AppData, } +/// Per-action output committed in the execution proof instance. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ActionInfo { + /// The action tree root derived from the action's compliance instances. + pub action_tree_root: Digest, + /// Application data for each consumed resource in this action. + pub consumed_resource_app_data: Vec, + /// Application data for each created resource in this action. + pub created_resource_app_data: Vec, +} + +/// Per-transaction output committed in the execution proof instance. +/// +/// Wraps one [`ActionInfo`] per action in the transaction. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TxInfo(pub Vec); + /// Public outputs of the execution proof, committed to the RISC0 journal. /// /// Downstream verifiers chain proofs by checking that /// `old_commitment_tree_root` and `old_nullifier_tree_root` of a later proof /// match the outputs of the preceding one. The committed `batch_aggregation_vk` -/// and `compliance_vk` make the verifying keys that were used during proof -/// verification an explicit part of the instance, binding them to the journal. +/// and `compliance_vk` make the verifying keys used during proof verification +/// an explicit part of the instance, binding them to the journal. +/// +/// Per-transaction resource app-data and action tree roots are structured in +/// [`tx_infos`]: one [`TxInfo`] per transaction, each containing one +/// [`ActionInfo`] per action. +/// +/// [`tx_infos`]: ExecutionProofInstance::tx_infos #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ExecutionProofInstance { /// Commitment tree root before the batch was executed. @@ -61,10 +99,8 @@ pub struct ExecutionProofInstance { pub new_commitment_root: Digest, /// Nullifier tree root after executing the batch. pub new_nullifier_tree_root: Digest, - /// Application data for each consumed resource in this execution batch. - pub consumed_resource_app_data: Vec, - /// Application data for each created resource in this execution batch. - pub created_resource_app_data: Vec, + /// Per-transaction structured output, one entry per transaction in the batch. + pub tx_infos: Vec, /// Verifying key for the batch aggregation circuit. pub batch_aggregation_vk: Digest, /// Verifying key for the compliance circuit. diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 28fc63cd..22aa289f 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -53,7 +53,7 @@ pub use crate::action::ActionExt; pub use crate::compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt}; #[cfg(feature = "transaction")] pub use crate::compliance_unit::ComplianceUnitExt; -pub use crate::execution_proof::{ActionInput, LogicVerifierInput, TxInput}; +pub use crate::execution_proof::{ActionInfo, ActionInput, LogicVerifierInput, TxInfo, TxInput}; #[cfg(feature = "transaction")] pub use crate::logic_proof::LogicVerifierInputsExt; pub use crate::merkle_path::MerklePathExt; diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index d9318f63..01b18f0c 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -2,7 +2,10 @@ use anoma_rm_risc0::{ action_tree::MerkleTree, compliance::{ComplianceInstanceExt, ComplianceInstanceWords}, delta_proof::{DeltaInstance, DeltaProof}, - execution_proof::{ActionInput, ExecutionProofInstance, ExecutionProofWitness, ResourceAppData}, + execution_proof::{ + ActionInfo, ActionInput, ExecutionProofInstance, ExecutionProofWitness, ResourceAppData, + TxInfo, + }, Digest, LogicInstance, }; use risc0_zkvm::guest::env; @@ -19,6 +22,8 @@ struct ActionLogicData { lp_instances_u32: Vec>, /// Verifying keys parallel to `lp_instances_u32`. lp_vks: Vec, + /// The action tree root derived from the compliance instances. + action_root: Digest, /// App-data for consumed resources (nullifier tags), in CU order. consumed_resource_app_data: Vec, /// App-data for created resources (commitment tags), in CU order. @@ -93,6 +98,7 @@ fn collect_action_logic(action: &ActionInput) -> ActionLogicData { ActionLogicData { lp_instances_u32, lp_vks, + action_root: root, consumed_resource_app_data, created_resource_app_data, } @@ -102,10 +108,8 @@ fn collect_action_logic(action: &ActionInput) -> ActionLogicData { struct TxVerificationData { /// Serialised aggregation instance ready for `env::verify`. agg_words: Vec, - /// App-data for consumed resources across all actions. - consumed_resource_app_data: Vec, - /// App-data for created resources across all actions. - created_resource_app_data: Vec, + /// Structured per-action output for the execution proof instance. + tx_info: TxInfo, } /// Serialises the batch-aggregation circuit journal as `u32` words for `env::verify`. @@ -134,15 +138,17 @@ fn aggregation_instance_words( let mut lp_vks = Vec::new(); let mut lp_instances_u32 = Vec::new(); - let mut consumed_resource_app_data = Vec::new(); - let mut created_resource_app_data = Vec::new(); + let mut action_infos = Vec::new(); for action in &tx.actions { let data = collect_action_logic(action); lp_vks.extend(data.lp_vks); lp_instances_u32.extend(data.lp_instances_u32); - consumed_resource_app_data.extend(data.consumed_resource_app_data); - created_resource_app_data.extend(data.created_resource_app_data); + action_infos.push(ActionInfo { + action_tree_root: data.action_root, + consumed_resource_app_data: data.consumed_resource_app_data, + created_resource_app_data: data.created_resource_app_data, + }); } let agg_words = risc0_zkvm::serde::to_vec(&( @@ -155,8 +161,7 @@ fn aggregation_instance_words( TxVerificationData { agg_words, - consumed_resource_app_data, - created_resource_app_data, + tx_info: TxInfo(action_infos), } } @@ -216,8 +221,7 @@ pub fn main() { let batch_agg_vk_risc0 = vk_to_risc0(&witness.batch_aggregation_vk); let compliance_vk = witness.compliance_vk; - let mut consumed_resource_app_data: Vec = Vec::new(); - let mut created_resource_app_data: Vec = Vec::new(); + let mut tx_infos: Vec = Vec::new(); for tx in &witness.transactions { // --- 3a. Delta proof --- @@ -263,8 +267,7 @@ pub fn main() { let tx_data = aggregation_instance_words(tx, &compliance_vk); env::verify(batch_agg_vk_risc0, &tx_data.agg_words) .expect("aggregation proof verification failed"); - consumed_resource_app_data.extend(tx_data.consumed_resource_app_data); - created_resource_app_data.extend(tx_data.created_resource_app_data); + tx_infos.push(tx_data.tx_info); } // ----------------------------------------------------------------------- @@ -305,8 +308,7 @@ pub fn main() { old_nullifier_tree_root: witness.old_nullifier_tree_root, new_commitment_root: commitment_tree.root(), new_nullifier_tree_root: nullifier_root, - consumed_resource_app_data, - created_resource_app_data, + tx_infos, batch_aggregation_vk: witness.batch_aggregation_vk, compliance_vk: witness.compliance_vk, }); diff --git a/arm_circuits/execution_proof/src/main.rs b/arm_circuits/execution_proof/src/main.rs index 170d5a3c..0afbc4d9 100644 --- a/arm_circuits/execution_proof/src/main.rs +++ b/arm_circuits/execution_proof/src/main.rs @@ -154,8 +154,7 @@ mod tests { old_nullifier_tree_root: d(2), new_commitment_root: d(3), new_nullifier_tree_root: d(4), - consumed_resource_app_data: vec![], - created_resource_app_data: vec![], + tx_infos: vec![], batch_aggregation_vk: vk(&BATCH_AGGREGATION_VK_BYTES), compliance_vk: vk(&COMPLIANCE_VK_BYTES), }; From 9b999acdde375d5cbe9008623811895573b90f75 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Mon, 13 Apr 2026 00:00:54 +0800 Subject: [PATCH 19/21] perf: reduce allocations in execution proof guest circuit --- .../execution_proof/methods/guest/src/main.rs | 148 ++++++++++-------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/arm_circuits/execution_proof/methods/guest/src/main.rs b/arm_circuits/execution_proof/methods/guest/src/main.rs index 01b18f0c..e09e69ac 100644 --- a/arm_circuits/execution_proof/methods/guest/src/main.rs +++ b/arm_circuits/execution_proof/methods/guest/src/main.rs @@ -30,68 +30,69 @@ struct ActionLogicData { created_resource_app_data: Vec, } -/// Processes a single action in one pass over its compliance instances: +/// Processes a single action's compliance instances and logic verifier inputs: /// -/// - Builds the ordered tag / logic-ref lists for the action tree. +/// - Builds the action tree and derives its root (one pass over CUs for tags). /// - Asserts that each input's `verifying_key` matches the corresponding /// `logic_ref` committed inside the compliance instance. /// - Serialises each [`LogicInstance`] for the aggregation proof. /// - Splits [`ResourceAppData`] into consumed and created buckets. fn collect_action_logic(action: &ActionInput) -> ActionLogicData { - let mut tags = Vec::new(); - let mut logic_refs = Vec::new(); + let n = action.compliance_instances.len(); - for ci in &action.compliance_instances { - // Ordered as [consumed, created] per CU to match proof construction. - tags.push(ci.consumed_nullifier); - logic_refs.push(ci.consumed_logic_ref); - tags.push(ci.created_commitment); - logic_refs.push(ci.created_logic_ref); - } - - let root = MerkleTree::from(tags.clone()) + // Pass 1: build tree tags (2 per CU) and compute the action tree root. + let tree_tags: Vec = action + .compliance_instances + .iter() + .flat_map(|ci| [ci.consumed_nullifier, ci.created_commitment]) + .collect(); + let root = MerkleTree::from(tree_tags) .root() .expect("action tree root"); - let mut lp_vks = Vec::new(); - let mut lp_instances_u32 = Vec::new(); - let mut consumed_resource_app_data = Vec::new(); - let mut created_resource_app_data = Vec::new(); + // Pass 2: process logic verifier inputs for each (tag, logic_ref) pair. + let mut lp_vks = Vec::with_capacity(2 * n); + let mut lp_instances_u32 = Vec::with_capacity(2 * n); + let mut consumed_resource_app_data = Vec::with_capacity(n); + let mut created_resource_app_data = Vec::with_capacity(n); - for (index, (tag, logic_ref)) in tags.iter().zip(logic_refs.iter()).enumerate() { - let is_consumed = index % 2 == 0; + for ci in &action.compliance_instances { + for (tag, logic_ref, is_consumed) in [ + (ci.consumed_nullifier, ci.consumed_logic_ref, true), + (ci.created_commitment, ci.created_logic_ref, false), + ] { + let input = action + .logic_verifier_inputs + .iter() + .find(|i| i.tag == tag) + .expect("logic verifier input not found for tag"); - let input = action - .logic_verifier_inputs - .iter() - .find(|i| i.tag == *tag) - .expect("logic verifier input not found for tag"); + assert_eq!( + input.verifying_key, logic_ref, + "verifying key does not match logic ref for tag" + ); - assert_eq!( - input.verifying_key, *logic_ref, - "verifying key does not match logic ref for tag" - ); + lp_instances_u32.push( + risc0_zkvm::serde::to_vec(&LogicInstance { + tag: input.tag, + is_consumed, + root, + app_data: input.app_data.clone(), + }) + .expect("serialize logic instance"), + ); + lp_vks.push(input.verifying_key); - lp_instances_u32.push( - risc0_zkvm::serde::to_vec(&LogicInstance { + let resource_app_data = ResourceAppData { tag: input.tag, - is_consumed, - root, + vk: input.verifying_key, app_data: input.app_data.clone(), - }) - .expect("serialize logic instance"), - ); - lp_vks.push(input.verifying_key); - - let resource_app_data = ResourceAppData { - tag: input.tag, - vk: input.verifying_key, - app_data: input.app_data.clone(), - }; - if is_consumed { - consumed_resource_app_data.push(resource_app_data); - } else { - created_resource_app_data.push(resource_app_data); + }; + if is_consumed { + consumed_resource_app_data.push(resource_app_data); + } else { + created_resource_app_data.push(resource_app_data); + } } } @@ -136,9 +137,14 @@ fn aggregation_instance_words( .map(ComplianceInstanceWords::from) .collect(); - let mut lp_vks = Vec::new(); - let mut lp_instances_u32 = Vec::new(); - let mut action_infos = Vec::new(); + let total_cus: usize = tx + .actions + .iter() + .map(|a| a.compliance_instances.len()) + .sum(); + let mut lp_vks = Vec::with_capacity(2 * total_cus); + let mut lp_instances_u32 = Vec::with_capacity(2 * total_cus); + let mut action_infos = Vec::with_capacity(tx.actions.len()); for action in &tx.actions { let data = collect_action_logic(action); @@ -189,8 +195,14 @@ pub fn main() { // Nullifiers and commitments are collected here in tx → action → CU // order for reuse in step 3c, avoiding a second pass over the witness. // ----------------------------------------------------------------------- - let mut nullifiers: Vec = Vec::new(); - let mut commitments: Vec = Vec::new(); + let total_cus: usize = witness + .transactions + .iter() + .flat_map(|tx| tx.actions.iter()) + .map(|a| a.compliance_instances.len()) + .sum(); + let mut nullifiers: Vec = Vec::with_capacity(total_cus); + let mut commitments: Vec = Vec::with_capacity(total_cus); for tx in &witness.transactions { for action in &tx.actions { for ci in &action.compliance_instances { @@ -203,8 +215,10 @@ pub fn main() { // Sort a copy of the nullifiers and check adjacent pairs for duplicates. // Sorting uses only integer comparisons (Digest is [u32; 8]), which is far // cheaper in the zkVM than HashSet whose SipHash has no RISC0 accelerator. + // The copy is necessary because `nullifiers` must stay in original order + // for zipping with `nullifier_witnesses` in step 3c. let mut sorted_nullifiers = nullifiers.clone(); - sorted_nullifiers.sort_by_key(|d| *d.as_words()); + sorted_nullifiers.sort_unstable_by_key(|d| *d.as_words()); for window in sorted_nullifiers.windows(2) { assert_ne!( window[0], window[1], @@ -221,7 +235,7 @@ pub fn main() { let batch_agg_vk_risc0 = vk_to_risc0(&witness.batch_aggregation_vk); let compliance_vk = witness.compliance_vk; - let mut tx_infos: Vec = Vec::new(); + let mut tx_infos: Vec = Vec::with_capacity(witness.transactions.len()); for tx in &witness.transactions { // --- 3a. Delta proof --- @@ -230,24 +244,20 @@ pub fn main() { // each compliance unit) and the delta instance (sum of delta EC points // from each compliance instance) directly from the ActionInput data. // No Transaction object or journal parsing is needed. - let msg: Vec = tx + let tx_cus: usize = tx .actions .iter() - .flat_map(|a| a.compliance_instances.iter()) - .flat_map(|ci| { - let mut bytes = Vec::with_capacity(64); - bytes.extend_from_slice(ci.consumed_nullifier.as_bytes()); - bytes.extend_from_slice(ci.created_commitment.as_bytes()); - bytes - }) - .collect(); - - let delta_points: Vec<_> = tx - .actions - .iter() - .flat_map(|a| a.compliance_instances.iter()) - .map(|ci| ci.delta_projective().expect("delta projective")) - .collect(); + .map(|a| a.compliance_instances.len()) + .sum(); + let mut msg = Vec::with_capacity(tx_cus * 64); + let mut delta_points = Vec::with_capacity(tx_cus); + for a in &tx.actions { + for ci in &a.compliance_instances { + msg.extend_from_slice(ci.consumed_nullifier.as_bytes()); + msg.extend_from_slice(ci.created_commitment.as_bytes()); + delta_points.push(ci.delta_projective().expect("delta projective")); + } + } let delta_instance = DeltaInstance::from_deltas(&delta_points).expect("delta instance"); let proof = DeltaProof::from_bytes(&tx.delta_proof).expect("deserialize delta proof"); From 046a941dfa8496f606e3af06cc8c423c31e22532 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 14 Apr 2026 17:50:20 +0800 Subject: [PATCH 20/21] perf: reorder struct fields for better page locality in execution proof guest --- arm/src/execution_proof.rs | 12 ++++++------ arm/src/indexed_merkle_tree.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/arm/src/execution_proof.rs b/arm/src/execution_proof.rs index 11d4f534..00b4d408 100644 --- a/arm/src/execution_proof.rs +++ b/arm/src/execution_proof.rs @@ -232,12 +232,16 @@ impl TxInput { /// [`compliance_vk`]: ExecutionProofWitness::compliance_vk #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExecutionProofWitness { - /// The transactions to execute and verify, in compact [`TxInput`] form. - pub transactions: Vec, /// Incremental commitment tree state before the batch. pub commitment_tree: IncrementalMerkleTree, /// Indexed nullifier tree root before the batch. pub old_nullifier_tree_root: Digest, + /// The transactions to execute and verify, in compact [`TxInput`] form. + pub transactions: Vec, + /// Verifying key for the batch aggregation circuit. + pub batch_aggregation_vk: Digest, + /// Verifying key for the compliance circuit. + pub compliance_vk: Digest, /// Nullifier insertion witnesses in transaction → action → compliance-unit /// order; one entry per compliance unit across the entire batch. /// @@ -245,8 +249,4 @@ pub struct ExecutionProofWitness { /// from the current nullifier tree root and returns the root after /// insertion, threading state forward to the next witness. pub nullifier_witnesses: Vec, - /// Verifying key for the batch aggregation circuit. - pub batch_aggregation_vk: Digest, - /// Verifying key for the compliance circuit. - pub compliance_vk: Digest, } diff --git a/arm/src/indexed_merkle_tree.rs b/arm/src/indexed_merkle_tree.rs index 98224d78..41680229 100644 --- a/arm/src/indexed_merkle_tree.rs +++ b/arm/src/indexed_merkle_tree.rs @@ -183,6 +183,8 @@ impl NonMembershipProof { pub struct InsertionWitness { /// Predecessor leaf before insertion: `predecessor.value < v < predecessor.next_value`. pub predecessor: IndexedLeaf, + /// `true` if the tree depth increased by 1 to accommodate the new leaf. + pub grew: bool, /// Merkle path for the predecessor, valid against the *insertion root*. /// /// The insertion root is `old_root` when `grew = false`, or @@ -192,8 +194,6 @@ pub struct InsertionWitness { /// Merkle path for the new leaf `(v → hi)`, valid against the /// *intermediate root* produced after rewriting the predecessor. pub new_leaf_path: MerklePath, - /// `true` if the tree depth increased by 1 to accommodate the new leaf. - pub grew: bool, } impl InsertionWitness { From bd33846e746437351aec0aa3ed70660c396d9523 Mon Sep 17 00:00:00 2001 From: Xuyang Song Date: Tue, 14 Apr 2026 18:54:39 +0800 Subject: [PATCH 21/21] docs: add perf.md with cycle profile and Groth16 benchmark results --- arm_circuits/execution_proof/perf.md | 83 ++ arm_circuits/execution_proof/profile.pb | 1672 +++++++++++++++++++++++ 2 files changed, 1755 insertions(+) create mode 100644 arm_circuits/execution_proof/perf.md create mode 100644 arm_circuits/execution_proof/profile.pb diff --git a/arm_circuits/execution_proof/perf.md b/arm_circuits/execution_proof/perf.md new file mode 100644 index 00000000..ff701f33 --- /dev/null +++ b/arm_circuits/execution_proof/perf.md @@ -0,0 +1,83 @@ +# Execution Proof Guest — Performance Report + +**Profile:** `profile.pb` +**Profile type:** cycles +**Total cycles:** 1,056,572 +**Accounted cycles:** 1,034,990 (97.96%) + +--- + +## Benchmark Results (wall time, Groth16 prover) + +### tx_count (1 compliance unit per transaction) + +| tx_count | time (median) | 95% CI | +|----------|--------------|--------| +| 1 | 8.520 s | [8.503 s, 8.536 s] | +| 2 | 10.328 s | [10.302 s, 10.351 s] | +| 4 | 15.304 s | [15.281 s, 15.328 s] | +| 8 | 25.526 s | [25.468 s, 25.591 s] | +| 16 | 45.412 s | [45.056 s, 45.693 s] | + +### compliance_count (1 transaction, N compliance units) + +| compliance_count | time (median) | 95% CI | +|-----------------|--------------|--------| +| 1 | 8.546 s | [8.529 s, 8.564 s] | +| 2 | 9.062 s | [9.046 s, 9.082 s] | +| 4 | 9.678 s | [9.661 s, 9.695 s] | + +--- + +## Top Functions by Self Cycles + +| Rank | Function | Flat % | Cum % | +|------|----------|--------|-------| +| 1 | `sys_bigint` | 25.99% | 26.12% | +| 2 | `sys_bigint2_3` | 15.85% | 15.91% | +| 3 | `[PageIn]` | 8.77% | 8.77% | +| 4 | `sys_bigint2_4` | 8.56% | 8.59% | +| 5 | `FieldElement::invert` | 5.65% | 25.04% | +| 6 | `sys_read_words` | 4.62% | 4.65% | +| 7 | `AffinePoint::mul` | 4.30% | 30.99% | +| 8 | `memcpy` | 4.25% | 5.58% | +| 9 | `memcmp` | 2.07% | 2.24% | +| 10 | `Vec::write_words` | 1.92% | 4.04% | +| 11 | `sys_write` | 1.90% | 2.21% | +| 12 | `[PageOut]` | 1.63% | 1.63% | +| 13 | `keccak::keccak_p` | 1.53% | 1.72% | +| 14 | `sys_sha_buffer` | 1.48% | 2.19% | +| 15 | `Scalar::invert` | 1.39% | 6.80% | +| 16 | `FdWriter::write_words` | 1.39% | 5.57% | +| 17 | `FdReader::read_words` | 0.98% | 5.66% | +| 18 | `VecVisitor::visit_seq` | 0.73% | 7.38% | +| 19 | `FieldElement::sqrt` | 0.63% | 3.03% | +| 20 | `ExpirableBlob::serialize` | 0.54% | 5.96% | + +--- + +## Key Call-Chain Hotspots (by Cumulative Cycles) + +| Call Chain | Cum Cycles | Cum % | +|------------|-----------|-------| +| `execution_proof_guest::main` | 1,036,034 | 98.06% | +| `DeltaProof::verify` | 655,789 | **62.07%** | +| `ecdsa::recover_from_digest` | 653,539 | 61.85% | +| `k256::mul::lincomb` | 450,931 | 42.68% | +| `AffinePoint::mul` | 327,425 | 30.99% | +| `ecdsa::hazmat::verify_prehashed` | 287,496 | 27.21% | +| `ProjectivePoint::to_affine` | 208,716 | 19.75% | +| `DeltaInstance::from_deltas` | 97,169 | 9.20% | +| `deserialize_struct` (serde) | 87,142 | 8.25% | +| `InsertionWitness::apply` | 28,201 | 2.67% | + +--- + +## Optimization Targets + +| Priority | Target | Cycle share | Approach | +|----------|--------|-------------|----------| +| **High** | `DeltaProof::verify` / ECDSA (62%) | ~656K | Aggregate delta proofs across all transactions into a single signature; reduces ECDSA verifications from N to 1 | +| **High** | `sys_bigint` + bigint2 syscalls (50% flat) | ~533K | Already using RISC0 accelerator — near floor for secp256k1 ECDSA | +| **Medium** | `DeltaInstance::from_deltas` (9.2% cum) | ~97K | Accumulate delta directly during the CU loop instead of collecting into a `Vec` first | +| **Low** | `FieldElement::invert` (5.6%) | ~60K | Batch-invert independent field elements using Montgomery's trick | diff --git a/arm_circuits/execution_proof/profile.pb b/arm_circuits/execution_proof/profile.pb new file mode 100644 index 00000000..de59035a --- /dev/null +++ b/arm_circuits/execution_proof/profile.pb @@ -0,0 +1,1672 @@ + + + +7 +> + + + + + + + + J + +  + + + + + +  + +: +  + + + +  +! + +"#$%&' +()*+,-.H +/0! + +12345678S + 9:;<=>?@A + BCDEFGHIJ +KLMNOPQR + STUVWXYZ[ + \]^_`abcd3 + +efghijklmn + BopqDEFGHIJ + + +SrTUVWXYZ[ + +BpqDEFGHIJ + + BqDEFGHIJ + + 9s;<=>?@A + tuvwxyz{| +tvwxyz{| + }/0! + +~< + +BDEFGHIJ + +ehijklmn + BoDEFGHIJ + + + +/0! + + +` +BDEFGHIJ +9;<=>?@A + +tvwxyz{|J +/0! + + BDEFGHIJ + + KLMNOPQR + STUVWXYZ[ + 9s;<=>?@A + BqDEFGHIJ + + tuvwxyz{| + \]^_`abcd3 +efghijklmn +BopqDEFGHIJ + +BpqDEFGHIJ + +SrTUVWXYZ[ +/0! + + SUVWXYZ[ + +tvwxyz{|J +}/0! + +BDEFGHIJ + + +BDEFGHIJ + +/0! + + 9;<=>?@A + \^_`abcd3 + SrUVWXYZ[ + efhijklmn +BopDEFGHIJ + + BpDEFGHIJ + +/0! + + /0! + + tvwxyz{| +F + twxyz{|2 + + 0! + + + + + + + + + + 0! + + + 0! + + + +0! + + + + + + + +0! + + 0! + + + + +@ +, + + 0! + +n + +0! + + 0! + +~ + 0! + + +V + 0! + +p +0! + +N +, + +0! + +^ +" +$ +& + ( +" $ +& + " +twxyz{| +0! + +0! + +twxyz{| + }0! + +}0! + + 0! + + + +" +$ +& + $ +& + ( +"" +$ +% + ~& + ! +0! +( +""# +0! +* +$# + 0! +* +$ - +&( +"* +$  +0! + +0! +$ +& +  + +twxyz{| + +  + +  +4 +, + +, + + twxyz{|d +n + twxyz{| + + +twxyz{|E + + +X" + + +  + twxyz{| + 0! + +B + + +, + +* + + +  + +X" + + + 0! + + + 0! + +$ + +  + + + + +0! + + + 0! + + + + + +twxyz{| + + + + + twxyz{| + + + + + + + twxyz{| + + + + 0! + + 0! + + + + + 0! + + + + + twxyz{| +4 + +$ +twxyz{| +}0! + + }0! + +T + +Z +l +twxyz{| + }0! + +}0! + + twxyz{|; + }0! + + }0! + + 0! + + +twxyz{| +}0! + + }0! + + + 0! + + + twxyz{| + }0! + + }0! + + +7 +h +< +H +twxyz{| +0! + + twxyz{|' +}0! + + }0! + + 0! + +< + twxyz{|r + + eijklmn  + BoEFGHIJ + 0! + + + + twxyz{| + 0! + + + 0! + +v + 9<=>?@A + BEFGHIJ + twxyz{| + }0! + + 0! + + + SVWXYZ[ +0! + + 9<=>?@A +0! + + 0! + + BEFGHIJ# + twxyz{|E + +BEFGHIJ + +0! + + eijklmn +BoEFGHIJ + + + + SVWXYZ[ + eijklmn  + BoEFGHIJ + BEFGHIJ + + BEFGHIJ + + + + 0! + +~ +0! +  + 0! + + + BEFGHIJ + + +0! + +9 +0! + +SVWXYZ[ +BEFGHIJ + 0! + +eijklmn +BoEFGHIJ + + +SVWXYZ[ +eijklmn +BoEFGHIJ + +BEFGHIJ# +eijklmn  +BoEFGHIJ + +SVWXYZ[ +twxyz{| +BEFGHIJ + +\_`abcd3 +BpEFGHIJ + +SrVWXYZ[ +efijklmn +BopEFGHIJ + +9<=>?@A +twxyz{| +}0! + + +! +܎ +M +$" +! + +twxyz{| + +twxyz{| +KMNOPQR +\]_`abcdg +BpqEFGHIJ +efgijklmn  +BopqEFGHIJ +SrTVWXYZ[ +STVWXYZ[ +BqEFGHIJ +tuwxyz{| +9s<=>?@A + +' +  + +` +BEFGHIJ +9<=>?@A + +BEFGHIJ + + +BEFGHIJ + eijklmn +BoEFGHIJ + + 9<=>?@A + + BEFGHIJ + + 0! + + + + KMNOPQR +tuwxyz{| +\]_`abcdg +SrTVWXYZ[ +efgijklmn  +BopqEFGHIJ +BpqEFGHIJ +9s<=>?@A +STVWXYZ[ +BqEFGHIJ + +` +9<=>?@A +BEFGHIJ + +BEFGHIJ + + +BEFGHIJ + twxyz{| + +twxyz{| +}0! + +0! + + +0! + +$ + +0! + +N +0! + + +0! + +' +  + + + KMNOPQR +BqEFGHIJ + +STVWXYZ[ +9s<=>?@A +tuwxyz{| +\]_`abcd3 +efgijklmn +BopqEFGHIJ + +BpqEFGHIJ + +SrTVWXYZ[ + 0! + + + +SVWXYZ[ +eijklmn +BoEFGHIJ + + twxyz{|4 + }0! +  + }0! + + + +] +  +\ +0! + +~< + BEFGHIJ + eijklmn +BoEFGHIJ + + +9 +BEFGHIJ +0! +9 + 0! + +eijklmn +BoEFGHIJ + +SVWXYZ[ +0! + + 0! + + 0! + +N + eijklmn +BoEFGHIJ + + BEFGHIJ + 1345678 + 9:<=>?@A + BCEFGHIJ + + + twxyz{|E + 0! + + 9<=>?@A + BEFGHIJ( + SVWXYZ[ + 3 +  +B EFGHIJ + 0! + +B EFGHIJ +e ijklmn +Bo EFGHIJ + +S VWXYZ[ +  +BEFGHIJ + , + t wxyz{|E + & + e ijklmn + Bo EFGHIJ + +' + eijklmn + BoEFGHIJ +  0! + + twxyz{| + BEFGHIJ + 0! + + z + t wxyz{| + +} 0! + + + } 0! + + y + t wxyz{| +  +  0! + +  +  0! + +  +  +  +  + t wxyz{|E +  +  0! + +  +  0! + +  + - + r +  0! + + 0! + +  +  +  0! + + ^ +t wxyz{| +  + > +  0! + + r +  + + 0! + + + 0! + + twxyz{| + t + t wxyz{|E + < + ^ + $ +  +  + t wxyz{|8 + r +  +  +   +  0! + +  0! + + + + + + + + + + + + + + t + + wxyz{| +} + + 0! + + } + + 0! + + + +  + + + ` + + + ~ + + +  + + + ~ +t + + wxyz{| + + +  + + +  + + +  + + +  + + + ? + + +  + + + + + + + + + + + +  + + 0! + +  + 0! + + 1 + + + + + + + + +[ + + + + + + + + + + + + + l + + +  +t + + wxyz{|U + } + + 0! + +} + + 0! + + + + * + + + b + + +  + + +  + + +   + + + * + + + r + + +  +  + + 0! + + t + + wxyz{| + + +  + + + + + + + + + + + +  + 0! + + + + + + + + + + + + t + + wxyz{| + + +  + + +  + + + l + + +   + + +  +t + + wxyz{|W +} + + 0! + + + + b + + +  + + +  + + + * + + + * + + + r + + + + + + + + + + + + + + + + + + + +w +  0! + + + + + + + + + +  +0! + + + + +? + +0 + + + + + t +wxyz{|< + + + + + + + +? + + + + + + + + +} +  +0! + + + + + + + + + +/ + + + + + + + + + +B + t + +wxyz{| + + + +  + + + + + + + + + +   +0! + + + + + + + + + + + + f +  +   +0! + +  +0! + + 0 + ? + t + +wxyz{|  +  +0! + + +0! + + A + + + + + + + + +/ + + + + + + + + + +B + t + +wxyz{| + + + + + l + + + + + + + + + + + + + + + + + + + +t + +wxyz{| +  +  +  +0! + + + + + + + + + + + + + + + + + + + + +t +  +wxyz{| + " +  +  + r +  +  +0! + + + + + + + + + +L +t +  +wxyz{|) +}   +0! + + " +   +   +0! + +    +0! + +   +0! +" +  +   +0! +" +  +   +0! + + + + + + + + + + +i +   +0! + +   +0! + +   +0! + +   +0! +  + + + + +  + + + + + + + + +  + + + + + + + + + + + D +  +    +0! + +   +0! + + * +   +t +  +wxyz{| +   +0! + + + + + + + + + +^ + + + + + + + + + + +t +  +wxyz{|2 +0! +% + 0! + + +  + 0! +  + + + +  +  +  + + +  +  +   + +  + +  + + + + +  + + + + + + . +   + + + +  + @ +  +  + + +  + + + + + & +  + + + h + + + + + + + + l +  +  + + * + t + |S + } + + + +  +  + b + +  + + + + +  +   +  + r +  + * + + + + + + + +  +t + | + + + + + +  +  +t |- +  +   + + + +  + + + + + +  +  + + + + + " + k +  +  + + + + > +  + + + % + +t | + + (8HP" "" "" "" ȅ"" ȅ"" ȅ"""""" "" +"" ""  φ""  φ"" φ"""" ǂ"" ǂ"" "" "" "" ǃ" " ǃ"" ǃ"" ǃ""" """"" ǃ" +" ǃ" " ǃ"" ǃ""  ǃ""!" +" "" " #" +" $" " %"" &"" '"" (" " )" " *" +" +" " ,"" -"" .""/" "0" " 1NJ" " 2NJ" " 3NJ" " 4NJ" +" 5NJ" " 6NJ"" 7NJ"" 8NJ"" 9"" :" " ;" " <" " =" +" >" " ?"" @"" A"" B"" C" " D" " E" " F" +" G" " H"" I"" J"" K"" L" " M" " N" +" O" " P"" Q"" R"" SΆ"" TΆ"" UΆ" " VΆ" " WΆ" +" XΆ" " YΆ"" ZΆ"" [Ά"" \"" ]"" ^" " _" " `" +" a" " b"" c"" d"" e"" f"" g"" h" " i" " j" +" k" " l"" m"" n"" o"" p"" q"" rΆ"" s"" tʅ"" uʅ"" vʅ" " wʅ" " xʅ" +" yʅ" " zʅ"" {ʅ"" |ʅ""}"" ~͊"" ͊" "͊" "͊" +"͊" "͊""͊""͊""""""֊""֊" "֊" "֊" +"֊" "֊""֊""֊"" """"""" "" "" +"" """""""""""""" "" "" +"" """""""""""""""""""֊""֊""֊""֊" "֊" "֊" +"֊" "֊""֊""֊""ʅ""ʅ""ʅ"" "" """"""Ά""Ά"""""""""" """"""""" "" "" +"" """""""ʅ"" """"""""""""" "" "" +"" """"""""" ""䓅""䓅" "䓅" +"䓅" "䓅""䓅""䓅""ʅ""""" "" +"" """"""" ""ن""ن""ن" "ن" +"ن" "ن""ن""ن""Ā""Ā" "Ā" +"Ā" "Ā""Ā""Ā""̑" "̑""̑" "̑" +"̑" "̑""̑""̑""̾"!"̾" "̾""̾" "̾" +"̾" "̾""̾""̾""І"""І"!"І" "І""І" "І" +"І" "І""І""І"""#"" """" "" +"" """""""̾"#"І"#" ""였""였" "였" +"였" "였""였""였""Ȝ"$"Ȝ""Ȝ" "Ȝ" +"Ȝ" "Ȝ""Ȝ""Ȝ""؆""؆"$"؆""؆" "؆" +"؆" "؆""؆""؆"" "" "$"̑"$"̾"$"І"$""$" " " """ "!""$"""" "" +"" """"""""""$"" "" +"" """"""""$"""" "" +"" """""""ـ""ـ"$"ـ" "ـ" +"ـ" "ـ""ـ""ـ""ࡃ"$"ࡃ""ࡃ" "ࡃ" +"ࡃ" "ࡃ""ࡃ""ࡃ"""""$"" "" +"" """""""ʅ""ʅ"$""$"""" "" +"" """"""""""$"" "" +"" """"""""""$"" "" +"" """""""襃"$"襃""襃" "襃" +"襃" "襃""襃""襃""ր""ր"$"ր" "ր" +"ր" "ր""ր""ր""ă"$"ă""ă" "ă" +"ă" "ă""ă""ă"""$"""" "" +"" """"""""$"""" "" +"" """"""""""$"" "" +"" """""""݀""݀"$"݀" "݀" +"݀" "݀""݀""݀"""$"""" "" +"" """"""""%"" "" +"" """"""" "%""&"" "" +"" """""""ԙ"'"ԙ"&"ԙ" "ԙ" +"ԙ" "ԙ""ԙ""ԙ""ȅ"'"ȅ"&"ȅ" "ȅ" +"ȅ" "ȅ""ȅ""Ɇ"("Ɇ"'"Ɇ"&"Ɇ" "Ɇ" +"Ɇ" "Ɇ""Ɇ""Ɇ""Ά")"Ά"("Ά"'"Ά"&"Ά" "Ά" +"Ά" "Ά""Ά""Ά"" ")" "(" "'" "&""*""(""'""&"" "" +"" """""""؆"*"؆"("؆"'"؆"&"ن"*"ن"("ن"'"ن"&"ʅ"*"ʅ"("ʅ"'"ʅ"&"ȅ"*"ȅ"("Ά"*""+""(""'""&"" "" +"" """""""",""'""&"" "" +"" """""""ݐ"-"ݐ"&"ݐ" "ݐ" +"ݐ" "ݐ""ݐ""ݐ""Ɓ"."Ɓ" "Ɓ" +"Ɓ" "Ɓ""Ɓ""Ɓ""ҁ"/"ҁ"."ҁ" "ҁ" +"ҁ" "ҁ""ҁ""ҁ""䓅"/"䓅"."ʅ"/"ʅ"."ƅ"0"ƅ""ƅ"/"ƅ"."ƅ" "ƅ" +"ƅ" "ƅ""ƅ""ƅ""Å"1"Å"0"Å""Å"/"Å"."Å" "Å" +"Å" "Å""Å""Å""ن""ن"1"ن"0"ن""ن"/"ن"."ن" "ن" +"ن" "ن""ن""ن""ʅ""ʅ"1"ʅ"0" "1" "0" "" "/" "."؆".""2"" "" +"" """"""" "2"䓅"2"ʅ"2""3""2"" "" +"" """""""䓅"3"ƅ"3"ƅ"2"Å"3"Å"2"ن"3"ن"2"ʅ"3" "3""4"" "" +"" """""""ʅ"4"Ƅ"5"Ƅ"4"Ƅ" "Ƅ" +"Ƅ" "Ƅ""Ƅ""Ƅ"""5""4""5""4" "5" "4"‡"6"‡"5"‡"4"‡" "‡" +"‡" "‡""‡""‡"""7"" "" +"" """""""ʅ"7" "7"䆋"8"䆋"7"䆋" "䆋" +"䆋" "䆋""䆋""䆋"" "8"ʊ"9"ʊ"7"ʊ" "ʊ" +"ʊ" "ʊ""ʊ""ʊ"""9""7""9""7"ʅ"9" "9"":""9""7"" "" +"" """""""Ά":"Ά"9"Ά"7" "" ":"":" """:"ʅ":"";"":""9""7"" "" +"" """""""";" ";"":""9""7""<"" "" +"" """"""""=""<"" "" +"" """""""Ά"="Ά"<""=""<""=""<""=""<"">""<"" "" +"" """"""""?"">""<"" "" +"" """"""" "?" ">" "<""@""?"">""<"" "" +"" """"""" "@""A"">""<"" "" +"" """"""""A"">""B""A"">""<"" "" +"" """"""" "B" "A"̇"C"̇"B"̇"A"̇">"̇"<"̇" "̇" +"̇" "̇""̇""̇"" "C"Ά"C"Ά"B"Ά"A"Ά">""C""B""C""B""A"">"ȇ"D"ȇ"B"ȇ"A"ȇ">"ȇ"<"ȇ" "ȇ" +"ȇ" "ȇ""ȇ""ȇ""Ά"D""D""D"ϊ"E"ϊ"B"ϊ"A"ϊ">"ϊ"<"ϊ" "ϊ" +"ϊ" "ϊ""ϊ""ϊ""Ά"E"ʅ"E"ʅ"B"ʅ"A"ʅ">"ʅ"<""E""E""B""A"">""<""E""E""B""A"">""<"д"F"д"B"д"A"д">"д"<"д" "д" +"д" "д""д""д""ڊ"G"ڊ"F"ڊ"B"ڊ"A"ڊ">"ڊ"<"ڊ" "ڊ" +"ڊ" "ڊ""ڊ""ڊ""׆"H"׆"G"׆"F"׆"B"׆"A"׆">"׆"<"׆" "׆" +"׆" "׆""׆""׆""ݐ"G"ݐ"F"ݐ"B"ݐ"A"ݐ">"ݐ"<"ސ"I"ސ"G"ސ"F"ސ"B"ސ"A"ސ">"ސ"<"ސ" "ސ" +"ސ" "ސ""ސ""ސ""ސ"J"ސ"I"ސ"G"ސ"F"ސ"B"ސ"A"ސ">"ސ"<"ސ" "ސ" +"ސ" "ސ""ސ""ސ""׆"K"׆"G"׆"F"׆"B"׆"A"׆">"׆"<"׆" "׆" +"׆" "׆""׆""׆""ʅ"G"ʅ"F"ȅ"F"ȅ"B"ȅ"A"ȅ">"ȅ"<""F""B""A"">""<""F""F""F"Ά"F""F""L""F""B""A"">""<"" "" +"" """""""֊"F"֊"B"֊"A"֊">"֊"<"ݐ""׆"""F""B""A"">""<""F""B""A"">""<""F""B""A"">""<""M"">""<"" "" +"" """"""""M" "M" "G" "F" "K" "J" "I" "-" "H""N"">""<"" "" +"" """"""""N""N"Ά"N""N"ʅ"N""N""N""O"">""<"" "" +"" """""""ȇ"O"Ά"O""O""O""P"">""<"" "" +"" """"""""Q"">""<"" "" +"" """"""""R""Q"">""<"" "" +"" """"""" "R" "Q"͊">"͊"<""S"">""<"" "" +"" """""""̇"S""S" "S""S"Ά"S""T"">""<"" "" +"" """"""""T""T"NJ">"NJ"<"荇"U"荇">"荇"<"荇" "荇" +"荇" "荇""荇""荇""ʅ"U" "U""U""U"Ά"U""V""U"">""<"" "" +"" " "" "" "" "V" "U" ">" "V"  "V" "V" "U" Ά"V" ф"W" ф"<" ф" " ф" +" ф" " ф"" ф"" ф"" ʅ"W" "X" "<" " " " +" " " "" "" "" "X" "X"  "T" "Y" " " " +" " " "" "" "" ʅ"Y"  "Y" "Z" "Y" " " " +" " " "" "" "" ʅ"Z" ؆"Y" "Y" " " " +" " " "" "" "" "[" " " " +" " " "" "" "" "[" ن"[" ʅ"[" ؆"["  "[" "Y" "[" " " " +" " " "" "" "" Ɇ"Y" Ɇ"[" Ά"Y" Ά"[" "Y" "["  "+" "Y" "[" ȅ"Y" ȅ"["  "*" ن"Y" ۂ"\" ۂ" " ۂ" +" ۂ" " ۂ"" ۂ"" ۂ"" ʅ"\" Ɔ"]" Ɔ"\" Ɔ" " Ɔ" +" Ɔ" " Ɔ"" Ɔ"" Ɔ"" "]" "\" ȅ"]" ȅ"\" ؆"]" ؆"\" ن"]" ن"\" ʅ"]" Ά"]" Ά"\" "]" "\"  "]"  "\" "^" "\" " " " +" " " "" "" +"" +"_" +"^" +"\" +" " +" +" +" " +"" +"" +"" +ʅ"_" +ʅ"^"  +"_"  +"^" +Ɔ"_" +Ɔ"^" +"_" +"^" +؆"_" +؆"^" +"_" +"^" +ȅ"_" +ȅ"^" +ن"_" +ن"^" +Ά"_" +Ά"^" +"`" +"_" +"^" +"\" +" " +" +" +" " +"" +"" +"" +"a" +"\" +" " +" +" +" " +"" +"" +"" +م"_" +م"a" +م"\" +م" " +م" +" +م" " +م"" +م"" +م"" +Ɔ"a" +"a" +ʅ"a"  +"a" +؆"a" +ȅ"a" +Ά"a" +ن"a" +"a" +"a" +҅"_" +҅"a" +҅"\" +҅" " +҅" +" +҅" " +҅"" +҅"" +҅"" +ӆ"b" +ӆ"\" +ӆ" " +ӆ" +" +ӆ" " +ӆ"" +ӆ"" +ӆ"" +"c" +" " +" +" +" " +"" +"" +""  +"c" +Ɇ"c" +؆"c" +"c" +Ά"c" +"c" +ʅ"c" +ȅ"c" +ن"c" +"d" +" " +" +" +" " +"" +"" +""  +"d" +ׂ"e" +ׂ"d" +ׂ" " +ׂ" +" +ׂ" " +ׂ"" +ׂ"" +ׂ"" +І"f" +І"e" +І"d" +І" " +І" +" +І" " +І"" +І"" +І"" +ʅ"e" +ʅ"d" +́"/" +́"d" +́" " ́" +" ́" " ́"" ́"" ́"" ׂ"/"  "e" І"/" ̆"g" ̆"e" ̆"/" ̆"d" ̆" " ̆" +" ̆" " ̆"" ̆"" ̆"" Ά"g" Ά"e" Ά"/" Ά"d"  "g" "g" "e" "/" "d" ؆"g" ؆"e" ؆"/" ؆"d" ρ"/" ρ"d" ρ" " ρ" +" ρ" " ρ"" ρ"" ρ"" Ձ"h" Ձ"/" Ձ"d" Ձ" " Ձ" +" Ձ" " Ձ"" Ձ"" Ձ"" "2" "h" "/" "d" " " " +" " " "" "" ""  "h" ׂ"2" ׂ"h" І"2" І"h" ʅ"h" ̆"2" ̆"h" "2" "h" ؆"2" ؆"h" Ά"2" Ά"h" "3" "2" "h" "/" "d" " " " +" " " "" "" "" ׂ"3" ̆"3" Ά"3" ؆"3" "3" І"3"  "f" ́"h" "" " " " +" " " "" "" "" 䩋"i" 䩋"" 䩋"" 䩋""  "i" "j" "i" "" "" "" ߋ"k" ߋ"j" ߋ"i" ߋ"" ߋ"" ߋ"" "i" "k" "j" "" "" ""  "k"  "j" "l" "" "l"  "`"  "l" "m" "l" "" "n" "m" "l" ""  "n"  "m" "o" "n" "m" "l" "" "p" "o" "n" "m" "l" "" І"q" І"p" І"o" І"n" І"m" І"l" І"" ݐ"q" ݐ"p" ݐ"o" ݐ"n" ݐ"m" ݐ"l"  "q"  "p"  "o" "r" "o" "n" "m" "l" "" ؆"r" ؆"o" ؆"n" ؆"m" ؆"l" "s" "l" ""  "s" م"s" م"l" Ɔ"s" Ɔ"l" "s" "l" ؆"s" ʅ"s" ʅ"l" Ά"s" Ά"l" ȅ"s" ȅ"l" ن"s" ن"l" "s" "l" "s" "t" "l" "" ʅ"t" "u" "l" "" "v" "u" "l" ""  "v"  "u" ن"v" ن"u" 쑆"w" 쑆"l" 쑆"" ݐ"w"  "w" ʆ"x" ʆ"l" ʆ"" ͆"y" ͆"x" ͆"l" ͆"" "x" ؆"x" ̺"z" ̺"l" ̺""  "z" ͆"{" ͆"l" ͆"* * * *** * + * *  * + *  *  *  * * * * * * * * * * * * * * * * *! *" * # *!$ *"% *#& *$' *%( *&) *'* *(+ *), **- *+. *,/ *-0 *.1 */2 *03 *14 *25 *36 *47 *58 *69 *7: *8; *9< *:= *;> *<? *=@ *>A *?B *@C *AD *BE *CF *DG *EH *FI *GJ *HK *IL *JM *KN *LO *MP *NQ *OR *PS *QT *RU *SV *TW *UX *VY *WZ *X[ *Y\ *Z] *[^ *\_ *]` *^a *_b *`c *ad *be *cf *dg *eh *fi *gj *hk *il *jm *kn *lo *mp *nq *or *ps *qt *ru *sv *tw *ux *vy *wz *x{ *y| *z} *{~ 22cycles2count2unknown2__start2risc0_zkvm::guest::env::init2memset2[PageIn]2 [PageOut]2sys_rand2main2std::rt::lang_start_internal2 std::rt::lang_start::{{closure}}21std::sys::backtrace::__rust_begin_short_backtrace2execution_proof_guest::main27anoma_rm_risc0::delta_proof::DeltaInstance::from_deltas2s>::from2U as subtle::ConditionallySelectable>::conditional_select2subtle::black_box28k256::arithmetic::projective::ProjectivePoint::to_affine2 +sys_bigint2-k256::arithmetic::field::FieldElement::invert2qcrypto_bigint::ct_choice:: for subtle::Choice>::from2memcpy2L::is_identity2|>::add22k256::arithmetic::projective::ProjectivePoint::add2&k256::arithmetic::affine_to_projective2(risc0_bigint2::ec::AffinePoint<_,C>::add2*k256::arithmetic::affine_to_bigint2_affine2/k256::arithmetic::field::FieldElement::to_bytes2O as risc0_zkvm::serde::serializer::WordWrite>::write_words25 as core::ops::drop::Drop>::drop2__rustc[8b64c29862d46f50]2j as serde_core::de::Deserializer>::deserialize_struct2i as serde_core::de::Deserializer>::deserialize_tuple2a::read_words2sys_read_words2'serde_core::de::SeqAccess::next_element2>::deserialize::VecVisitor as serde_core::de::Visitor>::visit_seq2>anoma_rm_core::nullifier_key::NullifierKeyCommitment::as_bytes2::try_call_once_slow2[::hash_words2sys_sha_buffer22risc0_zkp::core::hash::sha::guest::copy_and_update20core::alloc::layout::Layout::is_size_align_valid2(anoma_rm_core::merkle_path::padding_leaf2memcmp2%risc0_zkvm::serde::serializer::to_vec2k as serde_core::ser::Serializer>::serialize_newtype_struct2>alloc::raw_vec::RawVecInner::reserve::do_reserve_and_handle2alloc::raw_vec::finish_grow2yanoma_rm_core::logic_instance::_::::serialize2anoma_rm_core::logic_instance::_::::serialize23anoma_rm_risc0::delta_proof::DeltaProof::from_bytes2ecdsa::Signature::from_bytes2k256:: for crypto_bigint::uint::Uint<8_usize>>::decode_field_bytes2v::delta_projective2$anoma_rm_core::utils::words_to_bytes2~>::from_encoded_point2v>::from_encoded_point2Bk256::arithmetic::field::field_8x32_risc0::FieldElement8x32R0::add2/anoma_rm_risc0::delta_proof::DeltaProof::verify2C::eq2Necdsa::recovery::>::recover_from_digest2 keccak::p16002keccak::keccak_p2k256::ecdsa:: for k256::arithmetic::affine::AffinePoint>::verify_prehashed2ecdsa::hazmat::verify_prehashed2(k256::arithmetic::scalar::Scalar::invert2%k256::arithmetic::scalar::Scalar::mul2J::to_affine2k256::arithmetic::mul::lincomb2(risc0_bigint2::ec::AffinePoint<_,C>::mul2 sys_bigint2_32memmove2compiler_builtins::mem::memmove2 sys_bigint2_42!k256::arithmetic::scalar_to_words2m>>::reduce2k256::arithmetic::projective:: for k256::arithmetic::affine::AffinePoint>::from2@::mul2=::to_repr2@::neg2Jcrypto_bigint::uint::neg_mod::>::neg_mod2Q::invert_vartime2C::ct_eq2n>::decompress2+k256::arithmetic::field::FieldElement::sqrt28block_buffer::BlockBuffer::digest_blocks2M::is_high2] as alloc::vec::spec_from_iter_nested::SpecFromIterNested>::from_iter2>::from2-anoma_rm_risc0::action_tree::MerkleTree::root2&risc0_zkvm::guest::env::verify::verify2[::hash_bytes2T::digest2!risc0_binfmt::hash::tagged_struct2G>::as_ref2}risc0_zkvm::claim::receipt::>::add2sys_verify_integrity2Fanoma_rm_risc0::incremental_merkle_tree::IncrementalMerkleTree::insert2anoma_rm_risc0::execution_proof::_::::serialize2e as risc0_zkvm::serde::serializer::WordWrite>::write_words2 sys_write2_::compress_slice2g as serde_core::ser::SerializeStruct>::serialize_field2,std::sys::sync::once::no_threads::Once::call2std::io::stdio::cleanup2-std::sync::once_lock::OnceLock::initialize2 risc0_zkvm::guest::env::finalize23risc0_zkvm::mmr::MerkleMountainAccumulator::root2anyhow::__private::format_err2(anyhow::error::::msg2"std::backtrace::Backtrace::capture2 +sys_getenv2.anyhow::error::::construct2N::digest2K>::try_from2Canyhow::error::::drop2anyhow::error::object_drop25risc0_zkvm::guest::env::batcher::KeccakBatcher::flush2Y::compress2sys_sha_compress21risc0_zkp::core::hash::sha::Block::as_half_blocks2sys_halt \ No newline at end of file