diff --git a/Cargo.lock b/Cargo.lock index 2fd99d37..4862941a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,10 +234,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror 2.0.17", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/Cargo.toml b/Cargo.toml index 557d909a..d62eb5ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "arm_core", "arm", "arm_tests/arm_test_witness", "arm_tests/arm_test_app", diff --git a/arm/Cargo.toml b/arm/Cargo.toml index bd816832..ef3ebe3e 100644 --- a/arm/Cargo.toml +++ b/arm/Cargo.toml @@ -10,6 +10,7 @@ repository = { workspace = true } [dependencies] # If you want to try (experimental) std support, add `features = [ "std" ]` to risc0-zkvm +arm-core = { package = "anoma-rm-core", path = "../arm_core" } risc0-zkvm = { version = "3.0.3", features = [ "std", "unstable", diff --git a/arm/src/action.rs b/arm/src/action.rs index 99c9bdde..5cc8afee 100644 --- a/arm/src/action.rs +++ b/arm/src/action.rs @@ -1,28 +1,40 @@ //! An action represents a set of compliance units and logic verifiers. +pub use arm_core::action::Action; + +use k256::ProjectivePoint; + use crate::{ action_tree::MerkleTree, - compliance::ComplianceInstance, - compliance_unit::ComplianceUnit, + compliance_unit::{ComplianceUnit, ComplianceUnitExt}, error::ArmError, - logic_proof::{LogicVerifier, LogicVerifierInputs}, + logic_proof::{LogicVerifier, LogicVerifierInputsExt}, + Digest, LogicVerifierInputs, }; -use k256::ProjectivePoint; -use risc0_zkvm::Digest; -use serde::{Deserialize, Serialize}; - -/// An action consists of compliance units and logic verifier inputs. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Action { - /// The compliance units in this action. - pub compliance_units: Vec, - /// The logic verifier inputs in this action. - pub logic_verifier_inputs: Vec, -} -impl Action { +/// Extension methods for actions that require zkvm/k256 functionality. +pub trait ActionExt { /// Creates a new Action from compliance units and logic verifiers. - pub fn new( + fn new( + compliance_units: Vec, + logic_verifiers: Vec, + ) -> Result + where + Self: Sized; + + /// Verifies all proofs and consistencies in the action. + fn verify(&self) -> Result<(), ArmError>; + + /// This function computes the delta of the action by summing up the deltas + /// of each compliance unit. + fn delta(&self) -> Result; + + /// Constructs logic verifiers from compliance units and logic verifier inputs. + fn get_logic_verifiers(&self) -> Result, ArmError>; +} + +impl ActionExt for Action { + fn new( compliance_units: Vec, logic_verifiers: Vec, ) -> Result { @@ -36,40 +48,55 @@ impl Action { }) } - /// Returns a reference to the compliance units. - pub fn get_compliance_units(&self) -> &Vec { - &self.compliance_units + fn verify(&self) -> Result<(), ArmError> { + for unit in &self.compliance_units { + unit.verify()?; + } + + let logic_verifiers = self.get_logic_verifiers()?; + for verifier in &logic_verifiers { + verifier.verify()?; + } + + Ok(()) } - /// Returns a reference to the logic verifier inputs. - pub fn get_logic_verifier_inputs(&self) -> &Vec { - &self.logic_verifier_inputs + fn delta(&self) -> Result { + self.compliance_units + .iter() + .try_fold(ProjectivePoint::IDENTITY, |acc, unit| { + Ok(acc + unit.delta()?) + }) } - /// Constructs logic verifiers from the action's compliance units and logic verifier inputs. - /// It also checks consistency between compliance instances and logic verifier inputs. - pub(crate) fn get_logic_verifiers(&self) -> Result, ArmError> { + fn get_logic_verifiers(&self) -> Result, ArmError> { let mut logic_verifiers = Vec::new(); - let compliance_intances = self + // Construct the action tree. + let tags: Vec = self .compliance_units .iter() - .map(|unit| unit.get_instance()) - .collect::, ArmError>>()?; - - // Construct the action tree - let tags: Vec = compliance_intances - .iter() - .flat_map(|instance| vec![instance.consumed_nullifier, instance.created_commitment]) + .flat_map(|unit| { + [ + unit.instance.consumed_nullifier, + unit.instance.created_commitment, + ] + }) .collect(); - let logics = compliance_intances + let logics: Vec = self + .compliance_units .iter() - .flat_map(|instance| vec![instance.consumed_logic_ref, instance.created_logic_ref]) - .collect::>(); + .flat_map(|unit| { + [ + unit.instance.consumed_logic_ref, + unit.instance.created_logic_ref, + ] + }) + .collect(); let action_tree = MerkleTree::from(tags.clone()); let root = action_tree.root()?; - // Match logic verifier inputs with the tags in the action tree + // Match logic verifier inputs with the tags in the action tree. if tags.len() != self.logic_verifier_inputs.len() { return Err(ArmError::TagNotFound); } @@ -95,42 +122,4 @@ impl Action { Ok(logic_verifiers) } - - /// Verifies all proofs and consistencies in the action. - pub fn verify(&self) -> Result<(), ArmError> { - for unit in self.compliance_units.iter() { - unit.verify()?; - } - - let logic_verifiers = self.get_logic_verifiers()?; - for verifier in logic_verifiers.iter() { - verifier.verify()?; - } - - Ok(()) - } - - /// This function computes the delta of the action by summing up the deltas - /// of each compliance unit. - pub fn delta(&self) -> Result { - self.compliance_units - .iter() - .try_fold(ProjectivePoint::IDENTITY, |acc, unit| { - Ok(acc + unit.delta()?) - }) - } - - /// Constructs the delta message by concatenating the delta messages - /// of each compliance unit. - pub fn get_delta_msg(&self) -> Result, ArmError> { - let mut msg = Vec::new(); - for unit in &self.compliance_units { - if let Ok(instance) = unit.get_instance() { - msg.extend_from_slice(&instance.delta_msg()); - } else { - return Err(ArmError::InvalidComplianceInstance); - } - } - Ok(msg) - } } diff --git a/arm/src/action_tree.rs b/arm/src/action_tree.rs index 6e0786f0..4a99e19a 100644 --- a/arm/src/action_tree.rs +++ b/arm/src/action_tree.rs @@ -2,10 +2,10 @@ use crate::{ error::ArmError, - merkle_path::{MerklePath, PADDING_LEAF}, - utils::hash_two, + merkle_path::{padding_leaf, MerklePath}, + utils::{core_to_risc0_digest, hash_two, risc0_to_core_digest}, + Digest, }; -use risc0_zkvm::sha::Digest; /// A Merkle tree structure. #[derive(Debug, Clone, PartialEq, Eq)] @@ -37,17 +37,17 @@ impl MerkleTree { .checked_next_power_of_two() .ok_or(ArmError::TreeTooLarge)?; let mut cur_layer = self.leaves.clone(); - cur_layer.resize(len, *PADDING_LEAF); + cur_layer.resize(len, padding_leaf()); while cur_layer.len() > 1 { cur_layer = cur_layer .chunks(2) - .map(|pair| hash_two(&pair[0], &pair[1])) + .map(|pair| hash_core(&pair[0], &pair[1])) .collect(); } Ok(cur_layer[0]) } - // Generate the merkle path for the current leave + // Generate the merkle path for the current leave. /// Generates the Merkle path for a given leaf in the Merkle tree. /// /// # Arguments @@ -67,7 +67,7 @@ impl MerkleTree { return Err(ArmError::EmptyTree); } - if *cur_leave == *PADDING_LEAF { + if *cur_leave == padding_leaf() { return Err(ArmError::InvalidLeaf); } @@ -77,7 +77,7 @@ impl MerkleTree { .checked_next_power_of_two() .ok_or(ArmError::TreeTooLarge)?; let mut cur_layer = self.leaves.clone(); - cur_layer.resize(len, *PADDING_LEAF); + cur_layer.resize(len, padding_leaf()); if let Some(position) = cur_layer.iter().position(|v| v == cur_leave) { let mut merkle_path = Vec::new(); fn build_merkle_path_inner( @@ -99,7 +99,7 @@ impl MerkleTree { let prev_layer = cur_layer .chunks(2) - .map(|pair| hash_two(&pair[0], &pair[1])) + .map(|pair| hash_core(&pair[0], &pair[1])) .collect(); build_merkle_path_inner(prev_layer, position / 2, path); @@ -123,3 +123,9 @@ impl From> for MerkleTree { MerkleTree::new(leaves) } } + +fn hash_core(left: &Digest, right: &Digest) -> Digest { + let left = core_to_risc0_digest(left); + let right = core_to_risc0_digest(right); + risc0_to_core_digest(hash_two(&left, &right)) +} diff --git a/arm/src/compliance.rs b/arm/src/compliance.rs index 3cbafe8f..3d3430b0 100644 --- a/arm/src/compliance.rs +++ b/arm/src/compliance.rs @@ -1,16 +1,16 @@ -//! Compliance module containing the compliance instance and witness. +//! Compliance module containing the compliance witness. -/// Size hard-coded to two resources per unit -const COMPLIANCE_INSTANCE_SIZE: usize = 56; +pub use arm_core::compliance::ComplianceInstanceWords; use crate::{ error::ArmError, - merkle_path::MerklePath, - nullifier_key::NullifierKey, + merkle_path::{MerklePath, MerklePathExt}, + nullifier_key::{NullifierKey, NullifierKeyExt}, resource::Resource, utils::{bytes_to_words, words_to_bytes}, + Digest, }; -use hex::FromHex; +pub use arm_core::compliance::ComplianceInstance; use k256::{ elliptic_curve::{ sec1::{FromEncodedPoint, ToEncodedPoint}, @@ -18,46 +18,7 @@ use k256::{ }, EncodedPoint, ProjectivePoint, Scalar, }; -use lazy_static::lazy_static; use rand::rngs::OsRng; -use risc0_zkvm::Digest; -use serde_with::serde_as; - -lazy_static! { - /// The initial root of the empty commitment tree is the hash of an empty string. - pub static ref INITIAL_ROOT: Digest = - Digest::from_hex("cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06") - .unwrap(); -} - -/// The compliance instance contains all public inputs to the compliance proof. -#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub struct ComplianceInstance { - /// The nullifier of the consumed resource. - pub consumed_nullifier: Digest, - /// The logic ref of the consumed resource. - pub consumed_logic_ref: Digest, - /// The commitment tree root for the consumed resource. - pub consumed_commitment_tree_root: Digest, - /// The commitment of the created resource. - pub created_commitment: Digest, - /// The logic ref of the created resource. - pub created_logic_ref: Digest, - /// The delta x coordinate of the created resource(use u32 array to avoid padding issues in risc0). - pub delta_x: [u32; 8], - /// The delta y coordinate of the created resource(use u32 array to avoid padding issues in risc0). - pub delta_y: [u32; 8], -} - -/// The compliance instance represented as an array of u32 words for -/// serialization(used in the aggregation circuit). -#[serde_as] -#[derive(serde::Serialize, serde::Deserialize)] -pub struct ComplianceInstanceWords { - /// The compliance instance as an array of u32 words. - #[serde_as(as = "[_; COMPLIANCE_INSTANCE_SIZE]")] - pub u32_words: [u32; COMPLIANCE_INSTANCE_SIZE], -} /// The compliance witness contains all private inputs to the compliance proof. #[derive(Clone, serde::Serialize, serde::Deserialize)] @@ -74,9 +35,6 @@ pub struct ComplianceWitness { pub created_resource: Resource, /// Random scalar for delta commitment pub rcv: Vec, - // TODO: If we want to add function privacy, include: - // pub input_resource_logic_cm_r: [u8; DATA_BYTES], - // pub output_resource_logic_cm_r: [u8; DATA_BYTES], } impl ComplianceWitness { @@ -112,7 +70,7 @@ impl ComplianceWitness { merkle_path, rcv: Scalar::random(&mut OsRng).to_bytes().to_vec(), nf_key, - ephemeral_root: *INITIAL_ROOT, + ephemeral_root: arm_core::compliance::initial_root(), } } @@ -246,7 +204,7 @@ impl Default for ComplianceWitness { ComplianceWitness { consumed_resource, created_resource, - ephemeral_root: *INITIAL_ROOT, + ephemeral_root: arm_core::compliance::initial_root(), merkle_path, rcv, nf_key, @@ -254,9 +212,14 @@ impl Default for ComplianceWitness { } } -impl ComplianceInstance { +/// Extension methods for ComplianceInstance requiring k256. +pub trait ComplianceInstanceExt { /// Converts the delta commitment from affine coordinates to a ProjectivePoint. - pub fn delta_projective(&self) -> Result { + fn delta_projective(&self) -> Result; +} + +impl ComplianceInstanceExt for ComplianceInstance { + fn delta_projective(&self) -> Result { let encoded_point = EncodedPoint::from_affine_coordinates( words_to_bytes(&self.delta_x).into(), words_to_bytes(&self.delta_y).into(), @@ -266,22 +229,18 @@ impl ComplianceInstance { .into_option() .ok_or(ArmError::InvalidDelta) } +} - /// Retrieves the delta message used for signing. - pub fn delta_msg(&self) -> Vec { - let mut msg = Vec::new(); - msg.extend_from_slice(self.consumed_nullifier.as_bytes()); - msg.extend_from_slice(self.created_commitment.as_bytes()); - msg - } +/// Extension methods to serialize compliance instances into risc0 journal bytes. +pub trait ComplianceInstanceJournalExt { + /// Serialize a compliance instance to the journal byte format. + fn to_journal(&self) -> Result, ArmError>; } -impl ComplianceInstanceWords { - /// Creates a ComplianceInstanceWords from a byte slice. - pub fn from_bytes(instance_bytes: &[u8]) -> Result { - let u32_words: [u32; COMPLIANCE_INSTANCE_SIZE] = bytes_to_words(instance_bytes) - .try_into() - .map_err(|_| ArmError::InstanceSerializationFailed)?; - Ok(ComplianceInstanceWords { u32_words }) +impl ComplianceInstanceJournalExt for ComplianceInstance { + fn to_journal(&self) -> Result, ArmError> { + let words = + risc0_zkvm::serde::to_vec(self).map_err(|_| ArmError::InstanceSerializationFailed)?; + Ok(arm_core::utils::words_to_bytes(&words).to_vec()) } } diff --git a/arm/src/compliance_unit.rs b/arm/src/compliance_unit.rs index 3bf64007..80382052 100644 --- a/arm/src/compliance_unit.rs +++ b/arm/src/compliance_unit.rs @@ -1,49 +1,39 @@ -//! Compliance unit module containing the compliance proof and instance. +//! Compliance unit module containing compliance verification helpers. + +pub use arm_core::compliance_unit::ComplianceUnit; + +use k256::ProjectivePoint; +use risc0_zkvm::InnerReceipt; use crate::{ - compliance::ComplianceInstance, + compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt, ComplianceWitness}, constants::COMPLIANCE_VK, error::ArmError, - proving_system::{journal_to_instance, verify as verify_proof}, }; -use k256::ProjectivePoint; -use risc0_zkvm::InnerReceipt; -use serde::{Deserialize, Serialize}; #[cfg(feature = "prove")] use crate::{ - compliance::ComplianceWitness, constants::COMPLIANCE_PK, proving_system::{prove, ProofType}, }; -/// A compliance unit consists of a compliance proof and its corresponding instance. -/// The vk is a constant in the compliance unit, so we don't place it here. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct ComplianceUnit { - /// The compliance proof (optional, would be absent when aggregation is enabled). - pub proof: Option>, - /// The serialized compliance instance. - pub instance: Vec, -} +/// Extension methods for compliance units that require zkvm/k256 functionality. +pub trait ComplianceUnitExt { + /// Verifies the compliance proof against the instance using the constant verifying key. + fn verify(&self) -> Result<(), ArmError>; -impl ComplianceUnit { - /// Creates a new compliance unit by proving the given witness. - /// Proving key and verifying key are constants, so we don't need to pass - /// them as parameters. Instance is generated by proving. - #[cfg(feature = "prove")] - pub fn create(witness: &ComplianceWitness, proof_type: ProofType) -> Result { - let (proof, instance) = prove(COMPLIANCE_PK, witness, proof_type)?; - Ok(ComplianceUnit { - proof: Some(proof), - instance, - }) - } + /// Obtains the delta from the compliance instance. + fn delta(&self) -> Result; - /// Verifies the compliance proof against the instance using the constant verifying key. - pub fn verify(&self) -> Result<(), ArmError> { + /// Retrieves the inner receipt from the compliance proof. + fn get_inner_receipt(&self) -> Result; +} + +impl ComplianceUnitExt for ComplianceUnit { + fn verify(&self) -> Result<(), ArmError> { if let Some(proof) = &self.proof { - verify_proof(&COMPLIANCE_VK, &self.instance, proof) + let instance_bytes = self.instance.to_journal()?; + crate::proving_system::verify(&COMPLIANCE_VK, &instance_bytes, proof) } else { Err(ArmError::ProofVerificationFailed( "Missing compliance proof".into(), @@ -51,18 +41,11 @@ impl ComplianceUnit { } } - /// Obtains the delta from the compliance instance. - pub fn delta(&self) -> Result { - self.get_instance()?.delta_projective() - } - - /// Retrieves the compliance instance from the serialized instance data. - pub fn get_instance(&self) -> Result { - journal_to_instance(&self.instance) + fn delta(&self) -> Result { + self.instance.delta_projective() } - /// Retrieves the inner receipt from the compliance proof. - pub fn get_inner_receipt(&self) -> Result { + fn get_inner_receipt(&self) -> Result { let inner: InnerReceipt = bincode::deserialize( self.proof .as_ref() @@ -72,3 +55,17 @@ impl ComplianceUnit { Ok(inner) } } + +/// Creates a new compliance unit by proving the given witness. +#[cfg(feature = "prove")] +pub fn create_compliance_unit( + witness: &ComplianceWitness, + proof_type: ProofType, +) -> Result { + let instance = witness.constrain()?; + let (proof, _instance_bytes) = prove(COMPLIANCE_PK, witness, proof_type)?; + Ok(ComplianceUnit { + proof: Some(proof), + instance, + }) +} diff --git a/arm/src/constants.rs b/arm/src/constants.rs index c2672009..66441d9c 100644 --- a/arm/src/constants.rs +++ b/arm/src/constants.rs @@ -1,8 +1,11 @@ //! Constants for compliance and padding logic proving and verification keys. -use hex::FromHex; +pub use arm_core::constants::{ + BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES, EMPTY_HASH_BYTES, PADDING_LOGIC_VK_BYTES, +}; use lazy_static::lazy_static; -use risc0_zkvm::Digest; + +use crate::Digest; /// Compliance proving key / compliance guest ELF binary pub const COMPLIANCE_PK: &[u8] = include_bytes!("../elfs/compliance-guest.bin"); @@ -15,17 +18,16 @@ pub const BATCH_AGGREGATION_PK: &[u8] = include_bytes!("../elfs/batch-aggregatio lazy_static! { /// compliance verification key / compliance image id pub static ref COMPLIANCE_VK: Digest = - Digest::from_hex("919e13001cd3319be5a5a7cb189203be083674acb3fff23d05aae9c3ed86314d") - .unwrap(); + Digest::try_from(COMPLIANCE_VK_BYTES.as_slice()).unwrap(); /// padding logic verification key / compliance image id pub static ref PADDING_LOGIC_VK: Digest = - Digest::from_hex("21fcc2fc2c07f9753405d3070f2488c67389f7d797b6f6e20a9f2529fe4a0bff") - .unwrap(); + Digest::try_from(PADDING_LOGIC_VK_BYTES.as_slice()).unwrap(); } #[cfg(feature = "aggregation")] lazy_static! { /// Batch aggregation verification key / Batch aggregation image id. - pub static ref BATCH_AGGREGATION_VK: Digest = Digest::from_hex("213b3f40d7c113c1a012072fcd791fa44bf5166a2300121630bd3228e2b00827").unwrap(); + pub static ref BATCH_AGGREGATION_VK: Digest = + Digest::try_from(BATCH_AGGREGATION_VK_BYTES.as_slice()).unwrap(); } diff --git a/arm/src/delta_proof.rs b/arm/src/delta_proof.rs index af170b54..887589df 100644 --- a/arm/src/delta_proof.rs +++ b/arm/src/delta_proof.rs @@ -265,3 +265,32 @@ mod tests { assert_eq!(witness, decoded); } } + +/// DeltaProof: serialize then deserialize via bincode must round-trip. +#[test] +fn delta_proof_bincode_roundtrip() { + use k256::elliptic_curve::rand_core::OsRng; + + let mut rng = OsRng; + let signing_key = SigningKey::random(&mut rng); + let witness = DeltaWitness { signing_key }; + let proof = DeltaProof::prove(b"roundtrip", &witness).unwrap(); + + let encoded = bincode::serialize(&proof).unwrap(); + let decoded: DeltaProof = bincode::deserialize(&encoded).unwrap(); + assert_eq!(proof, decoded); +} + +/// DeltaWitness: serialize then deserialize via bincode must round-trip. +#[test] +fn delta_witness_bincode_roundtrip() { + use k256::elliptic_curve::rand_core::OsRng; + + let mut rng = OsRng; + let signing_key = SigningKey::random(&mut rng); + let witness = DeltaWitness { signing_key }; + + let encoded = bincode::serialize(&witness).unwrap(); + let decoded: DeltaWitness = bincode::deserialize(&encoded).unwrap(); + assert_eq!(witness, decoded); +} diff --git a/arm/src/error.rs b/arm/src/error.rs index 9c799267..1e3196d4 100644 --- a/arm/src/error.rs +++ b/arm/src/error.rs @@ -1,83 +1,3 @@ //! Arm-specific error types. -#![allow(missing_docs)] -use thiserror::Error; -#[derive(Debug, Error, Clone, PartialEq, Eq)] -pub enum ArmError { - #[error("Invalid resource kind")] - InvalidResourceKind, - #[error("Invalid resource serialization")] - InvalidResourceSerialization, - #[error("Invalid resource deserialization")] - InvalidResourceDeserialization, - #[error("Invalid nullifier key")] - InvalidNullifierKey, - #[error("Invalid delta")] - InvalidDelta, - #[error("Invalid signature")] - InvalidSignature, - #[error("Invalid signing key")] - InvalidSigningKey, - #[error("Invalid public key")] - InvalidPublicKey, - #[error("Serialization error")] - SerializationError, - #[error("Deserialization error")] - DeserializationError, - #[error("Journal decode error")] - JournalDecodingError, - #[error("Inner receipt deserialization error")] - InnerReceiptDeserializationError, - #[error("Unsupported proof type")] - UnsupportedProofType, - #[error("Failed to write witness")] - WriteWitnessFailed, - #[error("Failed to build prover environment")] - BuildProverEnvFailed, - #[error("Verifying key mismatch")] - VerifyingKeyMismatch, - #[error("Tag not found")] - TagNotFound, - #[error("Delta proof verification failed")] - DeltaProofVerificationFailed, - #[error("Expected delta proof, but found witness")] - ExpectedDeltaProof, - #[error("Invalid resource value reference")] - InvalidResourceValueRef, - #[error("Invalid leaf")] - InvalidLeaf, - #[error("Failed to generate proof with error: {0}")] - ProveFailed(String), - #[error("Proof verification failed with return code {0}")] - ProofVerificationFailed(String), - #[error("Invalid compliance instance")] - InvalidComplianceInstance, - #[error("Delta proof generation failed")] - DeltaProofGenerationFailed, - #[error("Invalid Random Commitment Value")] - InvalidRcv, - #[error("Encryption failed")] - EncryptionFailed, - #[error("Decryption failed")] - DecryptionFailed, - #[error("Instance serialization failed")] - InstanceSerializationFailed, - #[error("Missing field: {0}")] - MissingField(&'static str), - #[error("Invalid encryption nonce")] - InvalidEncryptionNonce, - #[error("Invalid resource nonce")] - InvalidResourceNonce, - #[error("Invalid nullifier commitment")] - InvalidNullifierCommitment, - #[error("Nullifier duplication detected")] - NullifierDuplication, - #[error("Empty tree")] - EmptyTree, - #[error("Invalid shared secret")] - InvalidSharedSecret, - #[error("Tree too large")] - TreeTooLarge, - #[error("Invalid delta proof: pls regenerate the proof")] - InvalidDeltaProof, -} +pub use arm_core::error::ArmError; diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 5cdc9694..0079cf16 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -27,4 +27,32 @@ pub mod resource_logic; pub mod transaction; pub mod utils; -pub use risc0_zkvm::Digest; +pub use arm_core::{ + action::Action, + compliance::{initial_root, ComplianceInstance, ComplianceInstanceWords}, + compliance_unit::ComplianceUnit, + constants::{ + BATCH_AGGREGATION_VK_BYTES, COMPLIANCE_VK_BYTES, EMPTY_HASH_BYTES, PADDING_LOGIC_VK_BYTES, + }, + delta_types::{DeltaProof as CoreDeltaProof, DeltaWitness as CoreDeltaWitness}, + error::ArmError, + logic_instance::{AppData, ExpirableBlob, LogicInstance, LogicVerifierInputs}, + merkle_path::{padding_leaf, MerklePath}, + nullifier_key::{NullifierKey, NullifierKeyCommitment}, + transaction::{Delta, Transaction}, + utils::{bytes_to_words, words_to_bytes}, + Digest, +}; + +#[cfg(feature = "transaction")] +pub use crate::action::ActionExt; +#[cfg(any(feature = "compliance_circuit", feature = "aggregation_circuit"))] +pub use crate::compliance::{ComplianceInstanceExt, ComplianceInstanceJournalExt}; +#[cfg(feature = "transaction")] +pub use crate::compliance_unit::ComplianceUnitExt; +#[cfg(feature = "transaction")] +pub use crate::logic_proof::LogicVerifierInputsExt; +pub use crate::merkle_path::MerklePathExt; +pub use crate::nullifier_key::NullifierKeyExt; +#[cfg(feature = "transaction")] +pub use crate::transaction::TransactionExt; diff --git a/arm/src/logic_instance.rs b/arm/src/logic_instance.rs index 100f3d02..43878d52 100644 --- a/arm/src/logic_instance.rs +++ b/arm/src/logic_instance.rs @@ -1,71 +1,3 @@ -//! Logic instance for ARM resource logic proofs. +//! Core logic instance types. -use risc0_zkvm::Digest; -use serde::{Deserialize, Serialize}; - -/// Represents a logic instance with its associated data. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct LogicInstance { - /// The logic instance's tag (either commitment or nullifier) - pub tag: Digest, - /// Indicates whether the logic instance is for a consumed resource. - pub is_consumed: bool, - /// The root digest of the logic instance. - pub root: Digest, - /// The application data associated with the logic instance. - pub app_data: AppData, -} - -/// Application data contains four different types of payloads. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct AppData { - /// The resource payload blobs. - pub resource_payload: Vec, - /// The discovery payload blobs. - pub discovery_payload: Vec, - /// The external payload blobs. - pub external_payload: Vec, - /// The application payload blobs. - pub application_payload: Vec, -} - -/// An expirable blob consists of a blob and a deletion criterion. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct ExpirableBlob { - /// The blob data as a vector of u32 words. - pub blob: Vec, - /// The deletion criterion for the blob. - pub deletion_criterion: u32, -} - -impl AppData { - /// Creates a new, empty AppData. - pub fn new() -> Self { - AppData { - resource_payload: Vec::new(), - discovery_payload: Vec::new(), - external_payload: Vec::new(), - application_payload: Vec::new(), - } - } - - /// Adds a resource payload blob with its deletion criterion. - pub fn add_resource_payload(&mut self, blob: ExpirableBlob) { - self.resource_payload.push(blob); - } - - /// Adds a discovery payload blob with its deletion criterion. - pub fn add_discovery_payload(&mut self, blob: ExpirableBlob) { - self.discovery_payload.push(blob); - } - - /// Adds an external payload blob with its deletion criterion. - pub fn add_external_payload(&mut self, blob: ExpirableBlob) { - self.external_payload.push(blob); - } - - /// Adds an application payload blob with its deletion criterion. - pub fn add_application_payload(&mut self, blob: ExpirableBlob) { - self.application_payload.push(blob); - } -} +pub use arm_core::logic_instance::{AppData, ExpirableBlob, LogicInstance, LogicVerifierInputs}; diff --git a/arm/src/logic_proof.rs b/arm/src/logic_proof.rs index 298ff628..869ee888 100644 --- a/arm/src/logic_proof.rs +++ b/arm/src/logic_proof.rs @@ -1,18 +1,21 @@ //! Logic proof structures and traits for proving and verifying logic statements. +use arm_core::logic_instance::LogicVerifierInputs; + use crate::{ constants::{PADDING_LOGIC_PK, PADDING_LOGIC_VK}, error::ArmError, - logic_instance::{AppData, LogicInstance}, - nullifier_key::{NullifierKey, NullifierKeyCommitment}, + logic_instance::LogicInstance, + nullifier_key::{NullifierKey, NullifierKeyCommitment, NullifierKeyExt}, proving_system::{journal_to_instance, verify as verify_proof}, resource::Resource, resource_logic::TrivialLogicWitness, utils::words_to_bytes, + Digest, }; use rand::rngs::OsRng; use rand::Rng; -use risc0_zkvm::{serde::to_vec, sha::Digest, InnerReceipt}; +use risc0_zkvm::{serde::to_vec, InnerReceipt}; use serde::{Deserialize, Serialize}; #[cfg(feature = "prove")] @@ -60,19 +63,6 @@ pub struct LogicVerifier { pub verifying_key: Digest, } -/// Inputs required to create a logic verifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct LogicVerifierInputs { - /// The tag (either commitment or nullifier) for the logic instance. - pub tag: Digest, - /// The verifying key for the logic proof. - pub verifying_key: Digest, - /// The application data associated with the logic instance. - pub app_data: AppData, - /// The logic proof (optional, would be absent when aggregation is enabled). - pub proof: Option>, -} - impl LogicVerifier { /// Verifies the logic proof against the instance using the provided verifying key. pub fn verify(&self) -> Result<(), ArmError> { @@ -92,34 +82,32 @@ impl LogicVerifier { } } -impl LogicVerifierInputs { +/// Extension methods on `LogicVerifierInputs` requiring risc0 serialization. +pub trait LogicVerifierInputsExt { /// Converts the LogicVerifierInputs into a LogicVerifier. - pub fn to_logic_verifier( - self, - is_consumed: bool, - root: Digest, - ) -> Result { - let instance_words = to_vec(&self.to_instance(is_consumed, root)) - .map_err(|_| ArmError::InstanceSerializationFailed)?; - Ok(LogicVerifier { - proof: self.proof, - instance: words_to_bytes(&instance_words).to_vec(), - verifying_key: self.verifying_key, - }) - } + fn to_logic_verifier(self, is_consumed: bool, root: Digest) -> Result; - /// Converts the LogicVerifierInputs into a LogicInstance. - fn to_instance(&self, is_consumed: bool, root: Digest) -> LogicInstance { - LogicInstance { + /// Retrieves the inner receipt from the logic proof. + fn get_inner_receipt(&self) -> Result; +} + +impl LogicVerifierInputsExt for LogicVerifierInputs { + fn to_logic_verifier(self, is_consumed: bool, root: Digest) -> Result { + let instance_words = to_vec(&LogicInstance { tag: self.tag, is_consumed, root, app_data: self.app_data.clone(), - } + }) + .map_err(|_| ArmError::InstanceSerializationFailed)?; + Ok(LogicVerifier { + proof: self.proof, + instance: words_to_bytes(&instance_words).to_vec(), + verifying_key: self.verifying_key, + }) } - /// Retrieves the inner receipt from the logic proof. - pub fn get_inner_receipt(&self) -> Result { + fn get_inner_receipt(&self) -> Result { let inner: InnerReceipt = bincode::deserialize( self.proof .as_ref() diff --git a/arm/src/merkle_path.rs b/arm/src/merkle_path.rs index b7d4388a..37abfd98 100644 --- a/arm/src/merkle_path.rs +++ b/arm/src/merkle_path.rs @@ -1,61 +1,28 @@ //! A Merkle path from a leaf to a root in a commitment/action tree. -use crate::utils::hash_two; -use hex::FromHex; -use lazy_static::lazy_static; -use risc0_zkvm::sha::Digest; -use serde::{Deserialize, Serialize}; +pub use arm_core::merkle_path::{padding_leaf, MerklePath}; -lazy_static! { - /// A constant padding leaf used in Merkle trees. - /// This is the hash of an empty string. - pub static ref PADDING_LEAF: Digest = - Digest::from_hex("cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06") - .unwrap(); -} - -/// A path from a position in a particular commitment tree to the root of that tree. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct MerklePath(pub Vec<(Digest, bool)>); - -impl MerklePath { - /// Constructs a Merkle path directly from a path and position. - pub fn from_path(auth_path: &[(Digest, bool)]) -> Self { - MerklePath(auth_path.to_vec()) - } +use crate::{ + utils::{core_to_risc0_digest, hash_two, risc0_to_core_digest}, + Digest, +}; +/// Extension methods for Merkle paths that require risc0 hashing. +pub trait MerklePathExt { /// Returns the root of the tree corresponding to this path applied to `leaf`. - pub fn root(&self, leaf: &Digest) -> Digest { - self.0.iter().fold( - *leaf, - |root, (p, leaf_is_on_right)| match leaf_is_on_right { - false => hash_two(&root, p), - true => hash_two(p, &root), - }, - ) - } - - /// Returns the length of the Merkle path. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Checks if the Merkle path is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Creates an empty Merkle path. - pub fn empty() -> Self { - MerklePath(vec![]) - } + fn root(&self, leaf: &Digest) -> Digest; } -impl Default for MerklePath { - fn default() -> Self { - MerklePath(vec![ - (Digest::default(), false); - 10 // COMMITMENT_TREE_DEPTH, only for testing - ]) +impl MerklePathExt for MerklePath { + fn root(&self, leaf: &Digest) -> Digest { + let leaf_r0 = core_to_risc0_digest(leaf); + let result = self.0.iter().fold(leaf_r0, |root, (p, leaf_is_on_right)| { + let p_r0 = core_to_risc0_digest(p); + match leaf_is_on_right { + false => hash_two(&root, &p_r0), + true => hash_two(&p_r0, &root), + } + }); + risc0_to_core_digest(result) } } diff --git a/arm/src/nullifier_key.rs b/arm/src/nullifier_key.rs index 8da36b73..04bdcd6c 100644 --- a/arm/src/nullifier_key.rs +++ b/arm/src/nullifier_key.rs @@ -1,75 +1,33 @@ -//! Nullifier key and its commitment +//! Nullifier key helpers requiring risc0 hashing and randomness. -use crate::error::ArmError; -use rand::{rngs::OsRng, Rng}; -use risc0_zkvm::sha::{Digest, Impl, Sha256, DIGEST_BYTES}; -use serde::{Deserialize, Serialize}; +pub use arm_core::nullifier_key::{NullifierKey, NullifierKeyCommitment}; -/// Nullifier key -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct NullifierKey([u8; DIGEST_BYTES]); +use arm_core::digest::DIGEST_BYTES; +use rand::{rngs::OsRng, Rng}; -impl NullifierKey { - /// Create a new nullifier key from bytes - pub fn new(nf_key: [u8; DIGEST_BYTES]) -> NullifierKey { - NullifierKey::from_bytes(nf_key) - } +/// Extension methods for nullifier keys that require risc0/rand. +pub trait NullifierKeyExt { + /// Compute the commitment to the nullifier key. + fn commit(&self) -> NullifierKeyCommitment; - /// Compute the commitment to the nullifier key - pub fn commit(&self) -> NullifierKeyCommitment { - NullifierKeyCommitment(*Impl::hash_bytes(self.inner())) - } + /// Generate a random nullifier key and its commitment. + fn random_pair() -> (NullifierKey, NullifierKeyCommitment) + where + Self: Sized; +} - /// Get the inner bytes of the nullifier key - pub fn inner(&self) -> &[u8] { - &self.0 - } +impl NullifierKeyExt for NullifierKey { + fn commit(&self) -> NullifierKeyCommitment { + use risc0_zkvm::sha::{Impl, Sha256}; - /// Create a nullifier key from bytes - pub fn from_bytes(bytes: [u8; DIGEST_BYTES]) -> NullifierKey { - NullifierKey(bytes) + let digest_r0 = *Impl::hash_bytes(self.inner()); + NullifierKeyCommitment::from_bytes(digest_r0.as_bytes()).unwrap() } - /// Generate a random nullifier key and its commitment - pub fn random_pair() -> (NullifierKey, NullifierKeyCommitment) { + fn random_pair() -> (NullifierKey, NullifierKeyCommitment) { let rng_bytes: [u8; DIGEST_BYTES] = OsRng.gen(); let nf_key = NullifierKey::from_bytes(rng_bytes); let nk_commitment = nf_key.commit(); (nf_key, nk_commitment) } } - -impl Default for NullifierKey { - fn default() -> Self { - NullifierKey([0u8; DIGEST_BYTES]) - } -} - -/// Commitment to nullifier key -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -pub struct NullifierKeyCommitment(Digest); - -impl NullifierKeyCommitment { - /// Get the inner nullifier key commitment - pub fn inner(&self) -> Digest { - self.0 - } - - /// Create a nullifier key commitment from bytes - pub fn from_bytes(bytes: &[u8]) -> Result { - let nk_cm: Digest = - Digest::try_from(bytes).map_err(|_| ArmError::InvalidNullifierCommitment)?; - Ok(NullifierKeyCommitment(nk_cm)) - } - - /// Get the bytes of the nullifier key commitment - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Default for NullifierKeyCommitment { - fn default() -> Self { - NullifierKey::default().commit() - } -} diff --git a/arm/src/proving_system.rs b/arm/src/proving_system.rs index 2e4a95bf..745a5bd0 100644 --- a/arm/src/proving_system.rs +++ b/arm/src/proving_system.rs @@ -1,7 +1,7 @@ //! Proving system interface for generating and verifying proofs. -use crate::error::ArmError; -use risc0_zkvm::{sha::Digest, InnerReceipt, Receipt}; +use crate::{error::ArmError, utils::core_to_risc0_digest, Digest}; +use risc0_zkvm::{InnerReceipt, Receipt}; use serde::de::DeserializeOwned; #[cfg(feature = "prove")] @@ -38,7 +38,8 @@ pub fn verify(verifying_key: &Digest, instance: &[u8], proof: &[u8]) -> Result<( bincode::deserialize(proof).map_err(|_| ArmError::InnerReceiptDeserializationError)?; let receipt = Receipt::new(inner, instance.to_vec()); - receipt.verify(*verifying_key).map_err(|err| { + let verifying_key = core_to_risc0_digest(verifying_key); + receipt.verify(verifying_key).map_err(|err| { ArmError::ProofVerificationFailed(format!("Proof verification failed: {}", err)) }) } diff --git a/arm/src/resource.rs b/arm/src/resource.rs index 43435d29..8d348c7e 100644 --- a/arm/src/resource.rs +++ b/arm/src/resource.rs @@ -18,7 +18,8 @@ const RESOURCE_BYTES: usize = DIGEST_BYTES use crate::{ error::ArmError, - nullifier_key::{NullifierKey, NullifierKeyCommitment}, + nullifier_key::{NullifierKey, NullifierKeyCommitment, NullifierKeyExt}, + utils::risc0_to_core_digest, }; use k256::{ @@ -28,9 +29,10 @@ use k256::{ use rand::rngs::OsRng; use rand::Rng; use risc0_zkvm::sha::{rust_crypto::Sha256 as Sha256Type, Impl, Sha256, DIGEST_BYTES}; -use risc0_zkvm::Digest; use serde::{Deserialize, Serialize}; +use crate::Digest; + /// Resource representation in the ARM system. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct Resource { @@ -172,7 +174,7 @@ impl Resource { offset += DIGEST_BYTES; assert_eq!(offset, RESOURCE_BYTES); // Now produce the hash - *Impl::hash_bytes(&bytes) + risc0_to_core_digest(*Impl::hash_bytes(&bytes)) } /// Compute the nullifier of the resource @@ -206,7 +208,7 @@ impl Resource { assert_eq!(offset, 4 * DIGEST_BYTES); - Ok(*Impl::hash_bytes(&bytes)) + Ok(risc0_to_core_digest(*Impl::hash_bytes(&bytes))) } else { Err(ArmError::InvalidNullifierKey) } @@ -280,7 +282,7 @@ impl Default for Resource { value_ref: Digest::default(), is_ephemeral: true, nonce: [0; DIGEST_BYTES], - nk_commitment: NullifierKeyCommitment::default(), + nk_commitment: NullifierKey::default().commit(), rand_seed: [0; DIGEST_BYTES], } } diff --git a/arm/src/resource_logic.rs b/arm/src/resource_logic.rs index bba0d334..6ede3018 100644 --- a/arm/src/resource_logic.rs +++ b/arm/src/resource_logic.rs @@ -2,9 +2,8 @@ use crate::{ error::ArmError, logic_instance::AppData, logic_instance::LogicInstance, - nullifier_key::NullifierKey, resource::Resource, + nullifier_key::NullifierKey, resource::Resource, Digest, }; -use risc0_zkvm::Digest; use serde::{Deserialize, Serialize}; /// Trait for logic circuits, defining the necessary methods. diff --git a/arm/src/transaction.rs b/arm/src/transaction.rs index 22a791c3..af7ffccb 100644 --- a/arm/src/transaction.rs +++ b/arm/src/transaction.rs @@ -1,68 +1,82 @@ -//! Transaction structure and associated methods. +//! Transaction extensions and associated methods. + +pub use arm_core::transaction::{Delta, Transaction}; + +use arm_core::delta_types::{DeltaProof as CoreDeltaProof, DeltaWitness as CoreDeltaWitness}; +use risc0_zkvm::InnerReceipt; #[cfg(feature = "aggregation")] use crate::{ - compliance::ComplianceInstanceWords, constants::{BATCH_AGGREGATION_PK, BATCH_AGGREGATION_VK, COMPLIANCE_VK}, proving_system::ProofType, - utils::{bytes_to_words, words_to_bytes}, + utils::{bytes_to_words, core_to_risc0_digest, words_to_bytes}, + ComplianceInstanceWords, }; #[cfg(feature = "aggregation")] use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, Receipt, VerifierContext}; -use risc0_zkvm::{Digest, InnerReceipt}; use crate::{ - action::Action, - compliance_unit::ComplianceUnit, + action::ActionExt, + compliance::ComplianceInstanceJournalExt, + compliance_unit::ComplianceUnitExt, delta_proof::{DeltaInstance, DeltaProof, DeltaWitness}, error::ArmError, - logic_proof::LogicVerifier, + logic_proof::{LogicVerifier, LogicVerifierInputsExt}, + Digest, }; -use serde::{Deserialize, Serialize}; - -/// Represents a transaction consisting of actions, delta proof, expected balance, -/// and optional aggregation proof. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Transaction { - /// The actions included in the transaction. - pub actions: Vec, - /// The delta proof, which can be either a witness for proving or a proof for verification. - pub delta_proof: Delta, - /// We can't support unbalanced transactions, so this is just a placeholder. - pub expected_balance: Option>, - /// The aggregation proof, if present, attesting to the validity of all individual proofs. - pub aggregation_proof: Option>, -} -/// Represents either a delta witness for proving or a delta proof for verification. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub enum Delta { - /// The delta witness used for proving the delta proof. - Witness(DeltaWitness), - /// The delta proof used for verification. - Proof(DeltaProof), -} +/// Extension methods for transactions that require zkvm/k256 functionality. +pub trait TransactionExt { + /// Generates the delta proof for the transaction if it contains a delta witness. + fn generate_delta_proof(self) -> Result; -impl Transaction { - /// Create a new transaction with the given actions and delta. - /// Delta proof is a deterministic process, no proving key is needed. - /// Delta instance can be constructed from the actions. - pub fn create(actions: Vec, delta: Delta) -> Self { - Transaction { - actions, - delta_proof: delta, - expected_balance: None, - aggregation_proof: None, - } - } + /// Verifies all the proofs and corresponding checks in the transaction. + fn verify(&self) -> Result<(), ArmError>; - /// Generates the delta proof for the transaction if it contains a delta witness. - pub fn generate_delta_proof(self) -> Result { + /// Returns the DeltaInstance constructed from the sum of all actions' deltas. + fn delta(&self) -> Result; + + /// Composes two transactions by concatenating their actions and combining their delta witnesses. + fn compose(tx1: Transaction, tx2: Transaction) -> Transaction + where + Self: Sized; + + /// Returns all compliance inner receipts in the transaction. + fn get_compliance_inner_receipts(&self) -> Result, ArmError>; + + /// Returns all logic inner receipts in the transaction. + fn get_logic_inner_receipts(&self) -> Result, ArmError>; + + /// Returns all compliance instances in serialized journal format. + fn get_compliance_instances(&self) -> Result>, ArmError>; + + /// Returns all logic verifiers in the transaction. + fn get_logic_verifiers(&self) -> Result, ArmError>; + + /// Returns all logic verifying keys and instances in the transaction. + fn get_logic_vks_and_instances(&self) -> Result<(Vec, Vec>), ArmError>; + + /// Aggregates all the transaction proofs. + #[cfg(feature = "aggregation")] + fn aggregate(&mut self, proof_type: ProofType) -> Result<(), ArmError>; + + /// Verifies the aggregated proof of the transaction. + #[cfg(feature = "aggregation")] + fn verify_aggregation(&self) -> Result<(), ArmError>; + + /// Constructs the aggregation instance by serializing all compliance and logic instances. + #[cfg(feature = "aggregation")] + fn construct_aggregation_instance(&self) -> Result, ArmError>; +} + +impl TransactionExt for Transaction { + fn generate_delta_proof(self) -> Result { match self.delta_proof { Delta::Witness(ref witness) => { - let msg = self.get_delta_msg()?; - let proof = DeltaProof::prove(&msg, witness)?; - let delta_proof = Delta::Proof(proof); + let witness = DeltaWitness::from_bytes(&witness.0)?; + let msg = self.get_delta_msg(); + let proof = DeltaProof::prove(&msg, &witness)?; + let delta_proof = Delta::Proof(CoreDeltaProof(proof.to_bytes())); Ok(Transaction { actions: self.actions, delta_proof, @@ -74,15 +88,15 @@ impl Transaction { } } - /// Verifies all the proofs and corresponding checks in the transaction. - pub fn verify(&self) -> Result<(), ArmError> { - match self.delta_proof { - Delta::Proof(ref proof) => { - let msg = self.get_delta_msg()?; + fn verify(&self) -> Result<(), ArmError> { + match &self.delta_proof { + Delta::Proof(proof) => { + let proof = DeltaProof::from_bytes(&proof.0)?; + let msg = self.get_delta_msg(); let instance = self.delta()?; - DeltaProof::verify(&msg, proof, instance)?; + DeltaProof::verify(&msg, &proof, instance)?; - // Check for nullifier duplication across all compliance units + // Check for nullifier duplication across all compliance units. self.nf_duplication_check()?; if self.aggregation_proof.is_some() { @@ -105,22 +119,7 @@ impl Transaction { } } - /// Inner check for nullifier duplication across all compliance units - pub fn nf_duplication_check(&self) -> Result<(), ArmError> { - let mut seen_nullifiers = std::collections::HashSet::new(); - for action in &self.actions { - for cu in action.get_compliance_units() { - let instance = cu.get_instance()?; - if !seen_nullifiers.insert(instance.consumed_nullifier) { - return Err(ArmError::NullifierDuplication); - } - } - } - Ok(()) - } - - /// Returns the DeltaInstance constructed from the sum of all actions' deltas. - pub fn delta(&self) -> Result { + fn delta(&self) -> Result { let mut points = Vec::with_capacity(self.actions.len()); for action in &self.actions { points.push(action.delta()?); @@ -128,56 +127,24 @@ impl Transaction { DeltaInstance::from_deltas(&points) } - /// Constructs the delta message by concatenating the delta messages - /// of each action. - pub fn get_delta_msg(&self) -> Result, ArmError> { - let mut msg = Vec::new(); - for action in &self.actions { - msg.extend(action.get_delta_msg()?); - } - Ok(msg) - } - - /// Composes two transactions by concatenating their actions and combining their delta witnesses. - pub fn compose(tx1: Transaction, tx2: Transaction) -> Transaction { + fn compose(tx1: Transaction, tx2: Transaction) -> Transaction { let mut actions = tx1.actions; actions.extend(tx2.actions); let delta = match (&tx1.delta_proof, &tx2.delta_proof) { (Delta::Witness(witness1), Delta::Witness(witness2)) => { - Delta::Witness(witness1.compose(witness2)) + let witness1 = DeltaWitness::from_bytes(&witness1.0) + .expect("invalid witness bytes in first transaction"); + let witness2 = DeltaWitness::from_bytes(&witness2.0) + .expect("invalid witness bytes in second transaction"); + let composed = witness1.compose(&witness2); + Delta::Witness(CoreDeltaWitness(composed.to_bytes())) } _ => panic!("Cannot compose transactions with different delta types"), }; Transaction::create(actions, delta) } - /// Returns `true` if any compliance or resource logic proof is `None`. - pub fn base_proofs_are_empty(&self) -> bool { - for a in self.actions.iter() { - if a.get_compliance_units().iter().any(|cu| cu.proof.is_none()) { - return true; - } - if a.get_logic_verifier_inputs() - .iter() - .any(|lp| lp.proof.is_none()) - { - return true; - } - } - - false - } - - /// Returns all compliance units in the transaction. - pub fn get_compliance_units(&self) -> Vec<&ComplianceUnit> { - self.actions - .iter() - .flat_map(|a| a.get_compliance_units().iter()) - .collect() - } - - /// Returns all compliance inner receipts in the transaction. - pub fn get_compliance_inner_receipts(&self) -> Result, ArmError> { + fn get_compliance_inner_receipts(&self) -> Result, ArmError> { let mut compliance_inner_receipts = Vec::new(); for cu in self.get_compliance_units() { let inner_receipt = cu.get_inner_receipt()?; @@ -186,12 +153,11 @@ impl Transaction { Ok(compliance_inner_receipts) } - /// Returns all logic inner receipts in the transaction. - pub fn get_logic_inner_receipts(&self) -> Result, ArmError> { + fn get_logic_inner_receipts(&self) -> Result, ArmError> { let mut logic_inner_receipts = Vec::new(); - for action in self.actions.iter() { + for action in &self.actions { let logic_inputs = action.get_logic_verifier_inputs(); - for lp in logic_inputs.iter() { + for lp in logic_inputs { let inner_receipt = lp.get_inner_receipt()?; logic_inner_receipts.push(inner_receipt); } @@ -199,17 +165,15 @@ impl Transaction { Ok(logic_inner_receipts) } - /// Returns all compliance instances in the transaction. - pub fn get_compliance_instances(&self) -> Vec> { + fn get_compliance_instances(&self) -> Result>, ArmError> { let mut result = Vec::new(); for cu in self.get_compliance_units() { - result.push(cu.instance.clone()); + result.push(cu.instance.to_journal()?); } - result + Ok(result) } - /// Returns all logic verifiers in the transaction. - pub fn get_logic_verifiers(&self) -> Result, ArmError> { + fn get_logic_verifiers(&self) -> Result, ArmError> { let mut result = Vec::new(); for action in &self.actions { let logic_verifiers = action.get_logic_verifiers()?; @@ -218,8 +182,7 @@ impl Transaction { Ok(result) } - /// Returns all logic verifying keys and instances in the transaction. - pub fn get_logic_vks_and_instances(&self) -> Result<(Vec, Vec>), ArmError> { + fn get_logic_vks_and_instances(&self) -> Result<(Vec, Vec>), ArmError> { let mut vks = Vec::new(); let mut instances = Vec::new(); for lp in self.get_logic_verifiers()? { @@ -228,14 +191,9 @@ impl Transaction { } Ok((vks, instances)) } -} -#[cfg(feature = "aggregation")] -impl Transaction { - /// Aggregates all the transaction proofs. - /// If aggregation is successful, `self` contains an aggregation proof and its - /// compliance and logic proofs are set to `None`. Else proofs are untouched. - pub fn aggregate(&mut self, proof_type: ProofType) -> Result<(), ArmError> { + #[cfg(feature = "aggregation")] + fn aggregate(&mut self, proof_type: ProofType) -> Result<(), ArmError> { // Check base proofs exist. if self.base_proofs_are_empty() { return Err(ArmError::ProveFailed( @@ -247,7 +205,7 @@ impl Transaction { let compliance_inner_receipts = self.get_compliance_inner_receipts()?; let logic_inner_receipts = self.get_logic_inner_receipts()?; let compliance_instances_u32: Vec = self - .get_compliance_instances() + .get_compliance_instances()? .iter() .map(|instance_bytes| ComplianceInstanceWords::from_bytes(instance_bytes)) .collect::, ArmError>>()?; @@ -258,7 +216,7 @@ impl Transaction { .map(|instance_bytes| bytes_to_words(instance_bytes)) .collect(); - // Add proofs as assumptions + // Add proofs as assumptions. let mut env_builder = ExecutorEnv::builder(); for inner_receipt in compliance_inner_receipts .into_iter() @@ -307,12 +265,12 @@ impl Transaction { self.aggregation_proof = Some(bincode::serialize(&agg_proof).map_err(|_| ArmError::SerializationError)?); - self.erase_base_proofs(); + erase_base_proofs(self); Ok(()) } - /// Verifies the aggregated proof of the transaction. - pub fn verify_aggregation(&self) -> Result<(), ArmError> { + #[cfg(feature = "aggregation")] + fn verify_aggregation(&self) -> Result<(), ArmError> { if let Some(proof) = &self.aggregation_proof { let instance = self.construct_aggregation_instance()?; @@ -321,8 +279,9 @@ impl Transaction { // Verify proof on the batch instance. let receipt = Receipt::new(inner_receipt, instance); + let batch_vk = core_to_risc0_digest(&BATCH_AGGREGATION_VK); - receipt.verify(*BATCH_AGGREGATION_VK).map_err(|err| { + receipt.verify(batch_vk).map_err(|err| { ArmError::ProofVerificationFailed(format!("Proof verification failed: {}", err)) }) } else { @@ -332,10 +291,10 @@ impl Transaction { } } - /// Constructs the aggregation instance by serializing all compliance and logic instances. - pub fn construct_aggregation_instance(&self) -> Result, ArmError> { + #[cfg(feature = "aggregation")] + fn construct_aggregation_instance(&self) -> Result, ArmError> { let compliance_instances_u32: Vec = self - .get_compliance_instances() + .get_compliance_instances()? .iter() .map(|instance_bytes| ComplianceInstanceWords::from_bytes(instance_bytes)) .collect::, ArmError>>()?; @@ -356,16 +315,16 @@ impl Transaction { Ok(words_to_bytes(&instance).to_vec()) } +} - // Replaces all compliance and resource logic proofs with `None`. - fn erase_base_proofs(&mut self) { - for a in self.actions.iter_mut() { - for cu in a.compliance_units.iter_mut() { - cu.proof = None; - } - for lp in a.logic_verifier_inputs.iter_mut() { - lp.proof = None; - } +#[cfg(feature = "aggregation")] +fn erase_base_proofs(tx: &mut Transaction) { + for a in &mut tx.actions { + for cu in &mut a.compliance_units { + cu.proof = None; + } + for lp in &mut a.logic_verifier_inputs { + lp.proof = None; } } } diff --git a/arm/src/utils.rs b/arm/src/utils.rs index ec1d8dc6..7c4bd587 100644 --- a/arm/src/utils.rs +++ b/arm/src/utils.rs @@ -1,50 +1,35 @@ //! Utility functions for byte and word conversions and hashing. -use risc0_zkvm::sha::{Digest, Impl, Sha256, DIGEST_WORDS}; - -/// Converts a byte slice to a vector of u32 words. -pub fn bytes_to_words(bytes: &[u8]) -> Vec { - let mut words = Vec::new(); - let mut iter = bytes.chunks_exact(4); - for chunk in iter.by_ref() { - let mut word = 0u32; - for &byte in chunk { - word = (word << 8) | (byte as u32); - } - words.push(u32::from_be(word)); - } - - let rem = iter.remainder(); - if !rem.is_empty() { - let mut arr = [0u8; 4]; - arr[..rem.len()].copy_from_slice(rem); - let mut word = 0u32; - for byte in arr { - word = (word << 8) | (byte as u32); - } - words.push(u32::from_be(word)); - } - words -} - -/// Converts a slice of u32 words to a byte slice. -pub fn words_to_bytes(words: &[u32]) -> &[u8] { - bytemuck::cast_slice(words) -} +pub use arm_core::utils::{bytes_to_words, words_to_bytes}; +use risc0_zkvm::sha::{Digest as Risc0Digest, Impl, Sha256, DIGEST_WORDS}; /// Hashes two digests together using SHA-256. -pub fn hash_two(left: &Digest, right: &Digest) -> Digest { - let mut words = Vec::with_capacity(2 * DIGEST_WORDS); - words.extend_from_slice(left.as_words()); - words.extend_from_slice(right.as_words()); +pub fn hash_two(left: &Risc0Digest, right: &Risc0Digest) -> Risc0Digest { + let mut words = [0u32; 2 * DIGEST_WORDS]; + words[..DIGEST_WORDS].copy_from_slice(left.as_words()); + words[DIGEST_WORDS..].copy_from_slice(right.as_words()); *Impl::hash_words(&words) } /// Hashes arbitrary bytes using SHA-256. -pub fn hash_bytes(bytes: &[u8]) -> Digest { +pub fn hash_bytes(bytes: &[u8]) -> Risc0Digest { *Impl::hash_bytes(bytes) } +/// Convert arm_core::Digest to risc0_zkvm::Digest (layouts are identical: both [u32; 8]). +pub fn core_to_risc0_digest(d: &arm_core::Digest) -> risc0_zkvm::Digest { + risc0_zkvm::Digest::new(*d.as_words()) +} + +/// Convert risc0_zkvm::Digest to arm_core::Digest. +pub fn risc0_to_core_digest(d: risc0_zkvm::Digest) -> arm_core::Digest { + let words: [u32; arm_core::digest::DIGEST_WORDS] = d + .as_words() + .try_into() + .expect("risc0 digest must have 8 words"); + arm_core::Digest::new(words) +} + #[test] fn test_bytes_to_words() { let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; diff --git a/arm_circuits/Cargo.lock b/arm_circuits/Cargo.lock index f39d956a..1676fd85 100644 --- a/arm_circuits/Cargo.lock +++ b/arm_circuits/Cargo.lock @@ -234,10 +234,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror 2.0.17", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/arm_circuits/batch_aggregation/methods/guest/Cargo.lock b/arm_circuits/batch_aggregation/methods/guest/Cargo.lock index f35f4f64..31fde582 100644 --- a/arm_circuits/batch_aggregation/methods/guest/Cargo.lock +++ b/arm_circuits/batch_aggregation/methods/guest/Cargo.lock @@ -29,10 +29,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/arm_circuits/compliance/methods/guest/Cargo.lock b/arm_circuits/compliance/methods/guest/Cargo.lock index 033ab006..b9d7ddc6 100644 --- a/arm_circuits/compliance/methods/guest/Cargo.lock +++ b/arm_circuits/compliance/methods/guest/Cargo.lock @@ -29,10 +29,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/arm_circuits/logic_test/methods/guest/Cargo.lock b/arm_circuits/logic_test/methods/guest/Cargo.lock index 77284e63..16ab75af 100644 --- a/arm_circuits/logic_test/methods/guest/Cargo.lock +++ b/arm_circuits/logic_test/methods/guest/Cargo.lock @@ -207,10 +207,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/arm_circuits/trivial_logic/methods/guest/Cargo.lock b/arm_circuits/trivial_logic/methods/guest/Cargo.lock index 65245952..abd97aa6 100644 --- a/arm_circuits/trivial_logic/methods/guest/Cargo.lock +++ b/arm_circuits/trivial_logic/methods/guest/Cargo.lock @@ -53,10 +53,24 @@ dependencies = [ "libc", ] +[[package]] +name = "anoma-rm-core" +version = "1.0.0" +dependencies = [ + "bincode", + "bytemuck", + "hex", + "hex-literal", + "serde", + "serde_with", + "thiserror", +] + [[package]] name = "anoma-rm-risc0" version = "1.1.1" dependencies = [ + "anoma-rm-core", "bincode", "bytemuck", "hex", diff --git a/arm_core/Cargo.toml b/arm_core/Cargo.toml new file mode 100644 index 00000000..d2e3a5f9 --- /dev/null +++ b/arm_core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "anoma-rm-core" +version = "1.0.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +serde_with = "3.14.1" +bincode = "1.3.3" +hex = "0.4" +hex-literal = "0.4" +bytemuck = { version = "1.12", features = ["derive"] } +thiserror = "2.0.6" diff --git a/arm_core/README.md b/arm_core/README.md new file mode 100644 index 00000000..79452919 --- /dev/null +++ b/arm_core/README.md @@ -0,0 +1,40 @@ +# Anoma Resource Machine Core Types + +Pure data types and serialization for the Anoma Shielded Resource Machine, with no dependencies on risc0-zkvm or k256. + +## Overview + +The `anoma-rm-core` crate provides the foundational types shared across the ARM crate family. It defines resources, transactions, compliance instances, Merkle paths, and related structures without pulling in cryptographic or zkVM dependencies. + +## Key Components + +- **`action`**: A set of compliance units and logic verifier inputs +- **`compliance`**: `ComplianceInstance` and `ComplianceInstanceWords` — public instance data committed to by compliance proofs +- **`compliance_unit`**: `ComplianceUnit` — pairs a proof with its compliance instance +- **`constants`**: Verification key bytes (image IDs) for compliance, padding logic, and batch aggregation circuits +- **`delta_types`**: `DeltaProof` and `DeltaWitness` — types for delta (value balance) proofs +- **`digest`**: `Digest` — a 32-byte hash wrapper with serialization support +- **`error`**: `ArmError` — shared error type +- **`logic_instance`**: `LogicInstance`, `LogicVerifierInputs`, `AppData`, `ExpirableBlob` — resource logic verification data +- **`merkle_path`**: `MerklePath` and `padding_leaf` — Merkle tree inclusion proof types +- **`nullifier_key`**: `NullifierKey` and `NullifierKeyCommitment` +- **`transaction`**: `Transaction` and `Delta` — the top-level transaction type +- **`utils`**: Conversion helpers (`bytes_to_words`, `words_to_bytes`) + +## Usage + +Add to your `Cargo.toml`: + +```toml +arm-core = { package = "anoma-rm-core", path = "../arm_core" } +``` + +## Documentation + +For more information, refer to: + +- [Anoma Resource Machine Specification](https://specs.anoma.net/latest/arch/system/state/resource_machine/index.html) + +## License + +Licensed under the Apache License 2.0. See [LICENSE](../LICENSE) for details. diff --git a/arm_core/src/action.rs b/arm_core/src/action.rs new file mode 100644 index 00000000..2d6feacf --- /dev/null +++ b/arm_core/src/action.rs @@ -0,0 +1,34 @@ +//! An action represents a set of compliance units and logic verifiers. + +use crate::{compliance_unit::ComplianceUnit, logic_instance::LogicVerifierInputs}; +use serde::{Deserialize, Serialize}; + +/// An action consists of compliance units and logic verifier inputs. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Action { + /// The compliance units in this action. + pub compliance_units: Vec, + /// The logic verifier inputs in this action. + pub logic_verifier_inputs: Vec, +} + +impl Action { + /// Returns a reference to the compliance units. + pub fn get_compliance_units(&self) -> &Vec { + &self.compliance_units + } + + /// Returns a reference to the logic verifier inputs. + pub fn get_logic_verifier_inputs(&self) -> &Vec { + &self.logic_verifier_inputs + } + + /// Constructs the delta message by concatenating the delta messages + /// of each compliance unit. + pub fn get_delta_msg(&self) -> Vec { + self.compliance_units + .iter() + .flat_map(|unit| unit.instance.delta_msg()) + .collect() + } +} diff --git a/arm_core/src/compliance.rs b/arm_core/src/compliance.rs new file mode 100644 index 00000000..e3509d2c --- /dev/null +++ b/arm_core/src/compliance.rs @@ -0,0 +1,125 @@ +//! Compliance module containing the compliance instance. + +use crate::{constants::EMPTY_HASH_BYTES, digest::Digest, error::ArmError, utils::bytes_to_words}; +use serde_with::serde_as; + +const COMPLIANCE_INSTANCE_SIZE: usize = 56; + +/// Returns the initial root of the empty commitment tree. +pub fn initial_root() -> Digest { + Digest::try_from(EMPTY_HASH_BYTES.as_slice()).unwrap() +} + +/// The compliance instance contains all public inputs to the compliance proof. +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct ComplianceInstance { + /// The nullifier of the consumed resource. + pub consumed_nullifier: Digest, + /// The logic ref of the consumed resource. + pub consumed_logic_ref: Digest, + /// The commitment tree root for the consumed resource. + pub consumed_commitment_tree_root: Digest, + /// The commitment of the created resource. + pub created_commitment: Digest, + /// The logic ref of the created resource. + pub created_logic_ref: Digest, + /// The delta x coordinate of the created resource(use u32 array to avoid padding issues in risc0). + pub delta_x: [u32; 8], + /// The delta y coordinate of the created resource(use u32 array to avoid padding issues in risc0). + pub delta_y: [u32; 8], +} + +/// The compliance instance represented as an array of u32 words for +/// serialization(used in the aggregation circuit). +#[serde_as] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ComplianceInstanceWords { + /// The compliance instance as an array of u32 words. + #[serde_as(as = "[_; COMPLIANCE_INSTANCE_SIZE]")] + pub u32_words: [u32; COMPLIANCE_INSTANCE_SIZE], +} + +impl ComplianceInstance { + /// Retrieves the delta message used for signing. + pub fn delta_msg(&self) -> Vec { + let mut msg = Vec::new(); + msg.extend_from_slice(self.consumed_nullifier.as_bytes()); + msg.extend_from_slice(self.created_commitment.as_bytes()); + msg + } +} + +impl From<&ComplianceInstance> for ComplianceInstanceWords { + fn from(instance: &ComplianceInstance) -> Self { + let mut words = [0u32; COMPLIANCE_INSTANCE_SIZE]; + words[0..8].copy_from_slice(instance.consumed_nullifier.as_words()); + words[8..16].copy_from_slice(instance.consumed_logic_ref.as_words()); + words[16..24].copy_from_slice(instance.consumed_commitment_tree_root.as_words()); + words[24..32].copy_from_slice(instance.created_commitment.as_words()); + words[32..40].copy_from_slice(instance.created_logic_ref.as_words()); + words[40..48].copy_from_slice(&instance.delta_x); + words[48..56].copy_from_slice(&instance.delta_y); + ComplianceInstanceWords { u32_words: words } + } +} + +impl ComplianceInstanceWords { + /// Creates a ComplianceInstanceWords from a byte slice. + pub fn from_bytes(instance_bytes: &[u8]) -> Result { + let u32_words: [u32; COMPLIANCE_INSTANCE_SIZE] = bytes_to_words(instance_bytes) + .try_into() + .map_err(|_| ArmError::InstanceSerializationFailed)?; + Ok(ComplianceInstanceWords { u32_words }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compliance_instance_to_words_roundtrip() { + let instance = ComplianceInstance { + consumed_nullifier: Digest::new([1, 2, 3, 4, 5, 6, 7, 8]), + consumed_logic_ref: Digest::new([9, 10, 11, 12, 13, 14, 15, 16]), + consumed_commitment_tree_root: Digest::new([17, 18, 19, 20, 21, 22, 23, 24]), + created_commitment: Digest::new([25, 26, 27, 28, 29, 30, 31, 32]), + created_logic_ref: Digest::new([33, 34, 35, 36, 37, 38, 39, 40]), + delta_x: [41, 42, 43, 44, 45, 46, 47, 48], + delta_y: [49, 50, 51, 52, 53, 54, 55, 56], + }; + + let words: ComplianceInstanceWords = (&instance).into(); + + // Each field occupies 8 words in order + assert_eq!( + &words.u32_words[0..8], + instance.consumed_nullifier.as_words() + ); + assert_eq!( + &words.u32_words[8..16], + instance.consumed_logic_ref.as_words() + ); + assert_eq!( + &words.u32_words[16..24], + instance.consumed_commitment_tree_root.as_words() + ); + assert_eq!( + &words.u32_words[24..32], + instance.created_commitment.as_words() + ); + assert_eq!( + &words.u32_words[32..40], + instance.created_logic_ref.as_words() + ); + assert_eq!(&words.u32_words[40..48], &instance.delta_x); + assert_eq!(&words.u32_words[48..56], &instance.delta_y); + } + + #[test] + fn compliance_instance_to_words_default_is_all_zeros() { + let instance = ComplianceInstance::default(); + let words: ComplianceInstanceWords = (&instance).into(); + assert_eq!(words.u32_words, [0u32; 56]); + } +} diff --git a/arm_core/src/compliance_unit.rs b/arm_core/src/compliance_unit.rs new file mode 100644 index 00000000..cc52b1df --- /dev/null +++ b/arm_core/src/compliance_unit.rs @@ -0,0 +1,11 @@ +use crate::compliance::ComplianceInstance; +use serde::{Deserialize, Serialize}; + +/// A compliance unit consists of a compliance proof and its structured instance. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct ComplianceUnit { + /// The compliance proof bytes (None when aggregation is enabled). + pub proof: Option>, + /// The compliance instance (structured public inputs). + pub instance: ComplianceInstance, +} diff --git a/arm_core/src/constants.rs b/arm_core/src/constants.rs new file mode 100644 index 00000000..837adf8f --- /dev/null +++ b/arm_core/src/constants.rs @@ -0,0 +1,18 @@ +//! Raw byte constants for verification keys. +use hex_literal::hex; + +/// Compliance verification key bytes. +pub const COMPLIANCE_VK_BYTES: [u8; 32] = + hex!("919e13001cd3319be5a5a7cb189203be083674acb3fff23d05aae9c3ed86314d"); + +/// Padding logic verification key bytes. +pub const PADDING_LOGIC_VK_BYTES: [u8; 32] = + hex!("21fcc2fc2c07f9753405d3070f2488c67389f7d797b6f6e20a9f2529fe4a0bff"); + +/// Batch aggregation verification key bytes. +pub const BATCH_AGGREGATION_VK_BYTES: [u8; 32] = + hex!("213b3f40d7c113c1a012072fcd791fa44bf5166a2300121630bd3228e2b00827"); + +/// Hash of the empty string - used for PADDING_LEAF and INITIAL_ROOT. +pub const EMPTY_HASH_BYTES: [u8; 32] = + hex!("cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06"); diff --git a/arm_core/src/delta_types.rs b/arm_core/src/delta_types.rs new file mode 100644 index 00000000..efe4dd4c --- /dev/null +++ b/arm_core/src/delta_types.rs @@ -0,0 +1,41 @@ +//! Opaque delta proof and witness types (no k256 dependency). + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Opaque 65-byte delta proof (signature bytes + recovery ID). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeltaProof(pub [u8; 65]); + +/// Opaque 32-byte delta witness (signing key bytes). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeltaWitness(pub [u8; 32]); + +impl Serialize for DeltaProof { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(&self.0) + } +} + +impl<'de> Deserialize<'de> for DeltaProof { + fn deserialize>(d: D) -> Result { + let bytes: Vec = Vec::deserialize(d)?; + bytes.try_into().map(DeltaProof).map_err(|v: Vec| { + serde::de::Error::custom(format!("expected 65 bytes, got {}", v.len())) + }) + } +} + +impl Serialize for DeltaWitness { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(&self.0) + } +} + +impl<'de> Deserialize<'de> for DeltaWitness { + fn deserialize>(d: D) -> Result { + let bytes: Vec = Vec::deserialize(d)?; + bytes.try_into().map(DeltaWitness).map_err(|v: Vec| { + serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len())) + }) + } +} diff --git a/arm_core/src/digest.rs b/arm_core/src/digest.rs new file mode 100644 index 00000000..46424b79 --- /dev/null +++ b/arm_core/src/digest.rs @@ -0,0 +1,110 @@ +//! Standalone Digest type, wire-compatible with `risc0_zkvm::sha::Digest`. + +use serde::{Deserialize, Serialize}; + +/// Number of bytes in a digest. +pub const DIGEST_BYTES: usize = 32; +/// Number of u32 words in a digest. +pub const DIGEST_WORDS: usize = 8; + +/// A SHA-256 digest represented as 8 little-endian u32 words. +/// +/// This type is wire-compatible with `risc0_zkvm::sha::Digest`: +/// both store `[u32; 8]` and expose bytes via `bytemuck::cast_slice`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Digest([u32; DIGEST_WORDS]); + +impl Digest { + /// Creates a new Digest from u32 words. + pub const fn new(words: [u32; DIGEST_WORDS]) -> Self { + Digest(words) + } + + /// Returns the digest as a slice of u32 words. + pub fn as_words(&self) -> &[u32; DIGEST_WORDS] { + &self.0 + } + + /// Returns the digest as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + bytemuck::cast_slice(&self.0) + } +} + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl TryFrom<&[u8]> for Digest { + type Error = &'static str; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != DIGEST_BYTES { + return Err("Invalid byte length for Digest"); + } + let mut words = [0u32; DIGEST_WORDS]; + for (i, chunk) in bytes.chunks_exact(4).enumerate() { + words[i] = u32::from_ne_bytes(chunk.try_into().unwrap()); + } + Ok(Digest(words)) + } +} + +impl hex::FromHex for Digest { + type Error = hex::FromHexError; + + fn from_hex>(hex: T) -> Result { + let bytes = hex::decode(hex)?; + Digest::try_from(bytes.as_slice()).map_err(|_| hex::FromHexError::InvalidStringLength) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_digest_roundtrip_bytes() { + let hex_str = "cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06"; + let digest = ::from_hex(hex_str).unwrap(); + let bytes = digest.as_bytes(); + let digest2 = Digest::try_from(bytes).unwrap(); + assert_eq!(digest, digest2); + } + + #[test] + fn test_digest_from_hex_nonzero() { + let hex_str = "cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06"; + let digest = ::from_hex(hex_str).unwrap(); + assert_ne!(digest, Digest::default()); + } + + #[test] + fn test_digest_default_is_zero() { + let digest = Digest::default(); + assert_eq!(digest.as_words(), &[0u32; DIGEST_WORDS]); + assert_eq!(digest.as_bytes(), &[0u8; DIGEST_BYTES]); + } + + #[test] + fn test_digest_words_bytes_consistency() { + let words = [ + 0x01020304u32, + 0x05060708, + 0x090a0b0c, + 0x0d0e0f10, + 0x11121314, + 0x15161718, + 0x191a1b1c, + 0x1d1e1f20, + ]; + let digest = Digest::new(words); + assert_eq!(digest.as_words(), &words); + let bytes = digest.as_bytes(); + assert_eq!(bytes.len(), DIGEST_BYTES); + let roundtrip = Digest::try_from(bytes).unwrap(); + assert_eq!(roundtrip, digest); + } +} diff --git a/arm_core/src/error.rs b/arm_core/src/error.rs new file mode 100644 index 00000000..9c799267 --- /dev/null +++ b/arm_core/src/error.rs @@ -0,0 +1,83 @@ +//! Arm-specific error types. +#![allow(missing_docs)] +use thiserror::Error; + +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum ArmError { + #[error("Invalid resource kind")] + InvalidResourceKind, + #[error("Invalid resource serialization")] + InvalidResourceSerialization, + #[error("Invalid resource deserialization")] + InvalidResourceDeserialization, + #[error("Invalid nullifier key")] + InvalidNullifierKey, + #[error("Invalid delta")] + InvalidDelta, + #[error("Invalid signature")] + InvalidSignature, + #[error("Invalid signing key")] + InvalidSigningKey, + #[error("Invalid public key")] + InvalidPublicKey, + #[error("Serialization error")] + SerializationError, + #[error("Deserialization error")] + DeserializationError, + #[error("Journal decode error")] + JournalDecodingError, + #[error("Inner receipt deserialization error")] + InnerReceiptDeserializationError, + #[error("Unsupported proof type")] + UnsupportedProofType, + #[error("Failed to write witness")] + WriteWitnessFailed, + #[error("Failed to build prover environment")] + BuildProverEnvFailed, + #[error("Verifying key mismatch")] + VerifyingKeyMismatch, + #[error("Tag not found")] + TagNotFound, + #[error("Delta proof verification failed")] + DeltaProofVerificationFailed, + #[error("Expected delta proof, but found witness")] + ExpectedDeltaProof, + #[error("Invalid resource value reference")] + InvalidResourceValueRef, + #[error("Invalid leaf")] + InvalidLeaf, + #[error("Failed to generate proof with error: {0}")] + ProveFailed(String), + #[error("Proof verification failed with return code {0}")] + ProofVerificationFailed(String), + #[error("Invalid compliance instance")] + InvalidComplianceInstance, + #[error("Delta proof generation failed")] + DeltaProofGenerationFailed, + #[error("Invalid Random Commitment Value")] + InvalidRcv, + #[error("Encryption failed")] + EncryptionFailed, + #[error("Decryption failed")] + DecryptionFailed, + #[error("Instance serialization failed")] + InstanceSerializationFailed, + #[error("Missing field: {0}")] + MissingField(&'static str), + #[error("Invalid encryption nonce")] + InvalidEncryptionNonce, + #[error("Invalid resource nonce")] + InvalidResourceNonce, + #[error("Invalid nullifier commitment")] + InvalidNullifierCommitment, + #[error("Nullifier duplication detected")] + NullifierDuplication, + #[error("Empty tree")] + EmptyTree, + #[error("Invalid shared secret")] + InvalidSharedSecret, + #[error("Tree too large")] + TreeTooLarge, + #[error("Invalid delta proof: pls regenerate the proof")] + InvalidDeltaProof, +} diff --git a/arm_core/src/lib.rs b/arm_core/src/lib.rs new file mode 100644 index 00000000..f6154a23 --- /dev/null +++ b/arm_core/src/lib.rs @@ -0,0 +1,16 @@ +//! ARM core types - no zkvm or k256 dependencies. + +pub mod action; +pub mod compliance; +pub mod compliance_unit; +pub mod constants; +pub mod delta_types; +pub mod digest; +pub mod error; +pub mod logic_instance; +pub mod merkle_path; +pub mod nullifier_key; +pub mod transaction; +pub mod utils; + +pub use digest::Digest; diff --git a/arm_core/src/logic_instance.rs b/arm_core/src/logic_instance.rs new file mode 100644 index 00000000..fd863b73 --- /dev/null +++ b/arm_core/src/logic_instance.rs @@ -0,0 +1,84 @@ +//! Logic instance for ARM resource logic proofs. + +use crate::digest::Digest; +use serde::{Deserialize, Serialize}; + +/// Represents a logic instance with its associated data. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct LogicInstance { + /// The logic instance's tag (either commitment or nullifier) + pub tag: Digest, + /// Indicates whether the logic instance is for a consumed resource. + pub is_consumed: bool, + /// The root digest of the logic instance. + pub root: Digest, + /// The application data associated with the logic instance. + pub app_data: AppData, +} + +/// Application data contains four different types of payloads. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct AppData { + /// The resource payload blobs. + pub resource_payload: Vec, + /// The discovery payload blobs. + pub discovery_payload: Vec, + /// The external payload blobs. + pub external_payload: Vec, + /// The application payload blobs. + pub application_payload: Vec, +} + +/// An expirable blob consists of a blob and a deletion criterion. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ExpirableBlob { + /// The blob data as a vector of u32 words. + pub blob: Vec, + /// The deletion criterion for the blob. + pub deletion_criterion: u32, +} + +/// Inputs required to create a logic verifier. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct LogicVerifierInputs { + /// The tag (either commitment or nullifier) for the logic instance. + pub tag: Digest, + /// The verifying key for the logic proof. + pub verifying_key: Digest, + /// The application data associated with the logic instance. + pub app_data: AppData, + /// The logic proof (optional, would be absent when aggregation is enabled). + pub proof: Option>, +} + +impl AppData { + /// Creates a new, empty AppData. + pub fn new() -> Self { + AppData { + resource_payload: Vec::new(), + discovery_payload: Vec::new(), + external_payload: Vec::new(), + application_payload: Vec::new(), + } + } + + /// Adds a resource payload blob with its deletion criterion. + pub fn add_resource_payload(&mut self, blob: ExpirableBlob) { + self.resource_payload.push(blob); + } + + /// Adds a discovery payload blob with its deletion criterion. + pub fn add_discovery_payload(&mut self, blob: ExpirableBlob) { + self.discovery_payload.push(blob); + } + + /// Adds an external payload blob with its deletion criterion. + pub fn add_external_payload(&mut self, blob: ExpirableBlob) { + self.external_payload.push(blob); + } + + /// Adds an application payload blob with its deletion criterion. + pub fn add_application_payload(&mut self, blob: ExpirableBlob) { + self.application_payload.push(blob); + } +} diff --git a/arm_core/src/merkle_path.rs b/arm_core/src/merkle_path.rs new file mode 100644 index 00000000..c58a85d3 --- /dev/null +++ b/arm_core/src/merkle_path.rs @@ -0,0 +1,44 @@ +//! A Merkle path from a leaf to a root in a commitment/action tree. + +use crate::digest::Digest; +use serde::{Deserialize, Serialize}; + +/// Depth of the commitment tree. +pub const COMMITMENT_TREE_DEPTH: usize = 10; + +/// Returns the constant padding leaf used in Merkle trees. +pub fn padding_leaf() -> Digest { + crate::compliance::initial_root() +} + +/// A path from a position in a particular commitment tree to the root of that tree. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MerklePath(pub Vec<(Digest, bool)>); + +impl MerklePath { + /// Constructs a Merkle path directly from a path and position. + pub fn from_path(auth_path: &[(Digest, bool)]) -> Self { + MerklePath(auth_path.to_vec()) + } + + /// Returns the length of the Merkle path. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Checks if the Merkle path is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Creates an empty Merkle path. + pub fn empty() -> Self { + MerklePath(vec![]) + } +} + +impl Default for MerklePath { + fn default() -> Self { + MerklePath(vec![(Digest::default(), false); COMMITMENT_TREE_DEPTH]) + } +} diff --git a/arm_core/src/nullifier_key.rs b/arm_core/src/nullifier_key.rs new file mode 100644 index 00000000..32c4bba9 --- /dev/null +++ b/arm_core/src/nullifier_key.rs @@ -0,0 +1,57 @@ +//! Nullifier key and its commitment + +use crate::{ + digest::{Digest, DIGEST_BYTES}, + error::ArmError, +}; +use serde::{Deserialize, Serialize}; + +/// Nullifier key +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NullifierKey([u8; DIGEST_BYTES]); + +impl NullifierKey { + /// Create a new nullifier key from bytes + pub fn new(nf_key: [u8; DIGEST_BYTES]) -> NullifierKey { + NullifierKey::from_bytes(nf_key) + } + + /// Get the inner bytes of the nullifier key + pub fn inner(&self) -> &[u8] { + &self.0 + } + + /// Create a nullifier key from bytes + pub fn from_bytes(bytes: [u8; DIGEST_BYTES]) -> NullifierKey { + NullifierKey(bytes) + } +} + +impl Default for NullifierKey { + fn default() -> Self { + NullifierKey([0u8; DIGEST_BYTES]) + } +} + +/// Commitment to nullifier key +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +pub struct NullifierKeyCommitment(Digest); + +impl NullifierKeyCommitment { + /// Get the inner nullifier key commitment + pub fn inner(&self) -> Digest { + self.0 + } + + /// Create a nullifier key commitment from bytes + pub fn from_bytes(bytes: &[u8]) -> Result { + let nk_cm: Digest = + Digest::try_from(bytes).map_err(|_| ArmError::InvalidNullifierCommitment)?; + Ok(NullifierKeyCommitment(nk_cm)) + } + + /// Get the bytes of the nullifier key commitment + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} diff --git a/arm_core/src/transaction.rs b/arm_core/src/transaction.rs new file mode 100644 index 00000000..89b9f299 --- /dev/null +++ b/arm_core/src/transaction.rs @@ -0,0 +1,91 @@ +//! Transaction structure and associated methods. + +use crate::{ + action::Action, + compliance_unit::ComplianceUnit, + delta_types::{DeltaProof, DeltaWitness}, + error::ArmError, +}; +use serde::{Deserialize, Serialize}; + +/// Represents a transaction consisting of actions, delta proof, expected balance, +/// and optional aggregation proof. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Transaction { + /// The actions included in the transaction. + pub actions: Vec, + /// The delta proof, which can be either a witness for proving or a proof for verification. + pub delta_proof: Delta, + /// We can't support unbalanced transactions, so this is just a placeholder. + pub expected_balance: Option>, + /// The aggregation proof, if present, attesting to the validity of all individual proofs. + pub aggregation_proof: Option>, +} + +/// Represents either a delta witness for proving or a delta proof for verification. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum Delta { + /// The delta witness used for proving the delta proof. + Witness(DeltaWitness), + /// The delta proof used for verification. + Proof(DeltaProof), +} + +impl Transaction { + /// Create a new transaction with the given actions and delta. + pub fn create(actions: Vec, delta: Delta) -> Self { + Transaction { + actions, + delta_proof: delta, + expected_balance: None, + aggregation_proof: None, + } + } + + /// Inner check for nullifier duplication across all compliance units + pub fn nf_duplication_check(&self) -> Result<(), ArmError> { + let mut seen_nullifiers = std::collections::HashSet::new(); + for action in &self.actions { + for cu in action.get_compliance_units() { + if !seen_nullifiers.insert(cu.instance.consumed_nullifier) { + return Err(ArmError::NullifierDuplication); + } + } + } + Ok(()) + } + + /// Constructs the delta message by concatenating the delta messages + /// of each action. + pub fn get_delta_msg(&self) -> Vec { + self.actions + .iter() + .flat_map(|a| a.get_delta_msg()) + .collect() + } + + /// Returns all compliance units in the transaction. + pub fn get_compliance_units(&self) -> Vec<&ComplianceUnit> { + self.actions + .iter() + .flat_map(|a| a.get_compliance_units().iter()) + .collect() + } + + /// Returns `true` if any compliance or resource logic proof is `None`. + pub fn base_proofs_are_empty(&self) -> bool { + for a in self.actions.iter() { + if a.get_compliance_units().iter().any(|cu| cu.proof.is_none()) { + return true; + } + if a.get_logic_verifier_inputs() + .iter() + .any(|lp| lp.proof.is_none()) + { + return true; + } + } + + false + } +} diff --git a/arm_core/src/utils.rs b/arm_core/src/utils.rs new file mode 100644 index 00000000..34092887 --- /dev/null +++ b/arm_core/src/utils.rs @@ -0,0 +1,46 @@ +//! Utility functions for byte and word conversions. + +/// Converts a byte slice to a vector of u32 words. +pub fn bytes_to_words(bytes: &[u8]) -> Vec { + let mut words = Vec::new(); + let mut iter = bytes.chunks_exact(4); + for chunk in iter.by_ref() { + let mut word = 0u32; + for &byte in chunk { + word = (word << 8) | (byte as u32); + } + words.push(u32::from_be(word)); + } + + let rem = iter.remainder(); + if !rem.is_empty() { + let mut arr = [0u8; 4]; + arr[..rem.len()].copy_from_slice(rem); + let mut word = 0u32; + for byte in arr { + word = (word << 8) | (byte as u32); + } + words.push(u32::from_be(word)); + } + words +} + +/// Converts a slice of u32 words to a byte slice. +pub fn words_to_bytes(words: &[u32]) -> &[u8] { + bytemuck::cast_slice(words) +} + +#[test] +fn test_bytes_to_words() { + let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; + let words = bytes_to_words(&bytes); + let expected_bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00]; + assert_eq!(expected_bytes, words_to_bytes(&words)); +} + +#[test] +fn test_words_to_bytes() { + let words = vec![0x01020304, 0x05060708]; + let bytes = words_to_bytes(&words); + assert_eq!(words, bytes_to_words(bytes)); +} diff --git a/arm_tests/arm_test_app/src/lib.rs b/arm_tests/arm_test_app/src/lib.rs index 6681a21e..902ad6bf 100644 --- a/arm_tests/arm_test_app/src/lib.rs +++ b/arm_tests/arm_test_app/src/lib.rs @@ -5,16 +5,17 @@ use anoma_rm_risc0::{ action::Action, action_tree::MerkleTree, - compliance::{ComplianceWitness, INITIAL_ROOT}, - compliance_unit::ComplianceUnit, + compliance::ComplianceWitness, + compliance_unit::create_compliance_unit, delta_proof::DeltaWitness, + initial_root, logic_proof::LogicProver, merkle_path::MerklePath, nullifier_key::NullifierKey, proving_system::ProofType, resource::Resource, transaction::{Delta, Transaction}, - Digest, + ActionExt, CoreDeltaWitness, Digest, NullifierKeyExt, TransactionExt, }; use anoma_rm_risc0_test_witness::TestLogicWitness; use hex::FromHex; @@ -103,13 +104,13 @@ pub fn create_an_action_with_multiple_compliances( let compliance_witness = ComplianceWitness { consumed_resource: consumed_resources[i], merkle_path: MerklePath::default(), // dummy path for test - ephemeral_root: *INITIAL_ROOT, + ephemeral_root: initial_root(), nf_key: nf_key.clone(), created_resource: created_resources[i], rcv: Scalar::ONE.to_bytes().to_vec(), // fixed rcv for test }; - let compliance_receipt = ComplianceUnit::create(&compliance_witness, proof_type).unwrap(); + let compliance_receipt = create_compliance_unit(&compliance_witness, proof_type).unwrap(); let consumed_resource_nf = consumed_resources[i].nullifier(&nf_key).unwrap(); let created_resource_cm = created_resources[i].commitment(); @@ -177,7 +178,10 @@ pub fn generate_test_transaction( proof_type: ProofType, ) -> Transaction { let (actions, delta_witness) = create_multiple_actions(n_actions, compliance_num, proof_type); - let tx = Transaction::create(actions, Delta::Witness(delta_witness)); + let tx = Transaction::create( + actions, + Delta::Witness(CoreDeltaWitness(delta_witness.to_bytes())), + ); let balanced_tx = tx.generate_delta_proof().unwrap(); balanced_tx.clone().verify().unwrap(); balanced_tx @@ -264,14 +268,14 @@ fn test_verify_aggregation_fails_for_incorrect_instances() { #[test] fn test_cannot_aggregate_invalid_proofs() { - use anoma_rm_risc0::logic_proof::LogicVerifierInputs; + use anoma_rm_risc0::LogicVerifierInputs; let tx = generate_test_transaction(2, 2, ProofType::Succinct); // Create a transaction with one invalid proof. let bad_lproof = LogicVerifierInputs { proof: tx.actions[0].logic_verifier_inputs[0].clone().proof, - verifying_key: Digest::from_bytes([66u8; 32]), //vec![666u32; 8], // Bad key. + verifying_key: Digest::try_from([66u8; 32].as_slice()).unwrap(), // Bad key. tag: tx.actions[0].logic_verifier_inputs[0].tag, app_data: tx.actions[0].logic_verifier_inputs[0].app_data.clone(), }; diff --git a/arm_tests/arm_test_witness/src/lib.rs b/arm_tests/arm_test_witness/src/lib.rs index b3f64ec6..0e017f98 100644 --- a/arm_tests/arm_test_witness/src/lib.rs +++ b/arm_tests/arm_test_witness/src/lib.rs @@ -2,7 +2,7 @@ use anoma_rm_risc0::{ error::ArmError, logic_instance::AppData, logic_instance::{ExpirableBlob, LogicInstance}, - merkle_path::MerklePath, + merkle_path::{MerklePath, MerklePathExt}, nullifier_key::NullifierKey, resource::Resource, resource_logic::LogicCircuit,