diff --git a/Cargo.lock b/Cargo.lock index b9b1494c..8d9991c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "risc0-zkvm", "rustler", "serde", + "serde_with", "sha3", "thiserror 2.0.12", ] @@ -1256,8 +1257,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1274,13 +1285,38 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.103", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.103", ] @@ -1343,7 +1379,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.103", @@ -4506,9 +4542,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ "base64", "chrono", @@ -4526,11 +4562,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.103", diff --git a/README.md b/README.md index cbd297b8..c309ef3e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This is a shielded resource machine implementation based on [Risc0-zkvm](https:/ - **`arm_circuits/`**: Demonstration circuits for arms and applications: - **compliance**: Basic compliance checking circuit - **trivial_logic**: Minimal logic circuit example, also used in padding resources + - **proof aggregation (batch_aggregation, sequential_aggregation)**: Circuits for single-run aggregation and IVC-based aggregation, respectively - **logic_test**: The logic circuit contains hardcoded data to cover all instance fields and is used only in tests - **counter**: The simple counter logic circuit - **kudo circuits(kudo_main, simple_kudo_denomination, simple_kudo_receive)**: kudo application circuits @@ -98,6 +99,10 @@ We have the following feature flags in arm lib: | `nif` | | Enables Erlang/Elixir NIF (Native Implemented Function) bindings | | `test_circuit` | | A simple circuit implementation for testing | | `test` | | Includes tx and action tests; some test APIs are available outside the arm lib(Binding lib and Elixir SDK). | +| `aggregation_circuit` | | A specific feature for (pcd-based) aggregation circuits | +| `aggregation` | `aggregation_circuit`, `transaction` | Enables proof aggregation (with constant-sized proofs by default) | +|`fast_aggregation` | `aggregation` | Faster aggregation with linear-sized proofs without compression +|`groth16_aggregation` | `aggregation` | Generates groth16 aggregation proofs (requires x86_64 machines) ### Usage Examples @@ -109,11 +114,18 @@ arm = "0.7.0" # Blockchain deployment with Groth16 proofs arm = { version = "0.7.0", default-features = false, features = ["groth16_prover", "transaction"] } +# Proof aggregation (a single succinct proof per transaction) +arm = { version = "0.7.0", features = ["aggregation"] } + +# Blockchain deployment with a Groth16 aggregation proof +arm = { version = "0.7.0", features = ["groth16_aggregation"] } + # Logic-circuit-only usage arm = { version = "0.7.0", default-features = false } # Elixir Anoma SDK arm = { version = "0.7.0", features = ["nif"] } +arm = { version = "0.7.0", features = ["nif"] } ``` @@ -140,4 +152,60 @@ arm-risc0/arm_circuits/compliance/methods/guest/target/riscv32im-risc0-zkvm-elf/ Note: The `unstable` feature of `risc0-zkvm` currently causes issues in circuits. This can be temporarily fixed by manually updating the tool. The problem will be fully resolved in the next release of RISC Zero. ```bash cargo install --force --git https://github.com/risc0/risc0 --tag v3.0.3 -Fexperimental cargo-risczero -``` \ No newline at end of file +``` + +## Proof aggregation +If a single transaction bundles too many resources, it is possible to aggregate all compliance and logic proofs into a single aggregation proof, attesting to the validity of them all. This reduces overall verification time and transaction size. + +### Before aggregation + Generate the transaction in the normal way in your workflow. But note that succinct proofs will yield faster aggregation. + + **Warning:** Bonsai does not support in-circuit verification of Groth16 proofs. You would need to generate succinct compliance and logic proofs instead. + + +### Prove and verify aggregations +You need to enable the `aggregation` feature to be able to prove or verify aggregations. + +The type of the aggregation proof is selected via a feature. It defaults to succinct stark proofs. For on-chain verification, you probably want to aggregate with the `groth16_aggregation` feature enabled. See the features table above for more information. + +We currently support two different aggregation strategies. The _batch_ strategy aggregates all proofs in the transaction in a single run. It is the default aggregation. + +```rust +use arm::transaction; + +let mut tx = generate_test_transaction(1); // Just a dummy tx, for illustration. + +// Upon succesful aggregation, compliance and resource logic proofs are erased. +assert!(tx.aggregate().is_ok()); +assert!(tx.verify_aggregation().is_ok()); +``` + +The _sequential_ strategy aggregates sequentially, in an IVC style. + +```rust +use arm::aggregation::AggregationStrategy; + +assert!(tx.aggregate_with_strategy(AggregationStrategy::Sequential).is_ok()); +``` + +**Warning:** Once again, aggregation erases all the individual proofs from `tx` and replaces them with the (single) aggregation proof in a dedicated field. This is why the transaction must be `mut`. This is true independently of the strategy used. + + +### External verification of the aggregation proof +Use + +```rust +tx.get_raw_aggregation_proof() +``` +to get the RISC0 `InnerReceipt` (the actual proof). The verifier would also need to derive the aggregation instance from `tx` on its own, and wrap both in a RISC0 `Receipt`. + +### Comparison + +**Strategy** | **Prover cost** | **Public input size** | **Aggregation scope** | **Memory efficient** +-------------|-----------------|-----------------------|-----------------------|---------------------- +**batch** | amortized among all tx proofs | linear in #{tx proofs} | fixed (single prover) | for RISC0 yes. In general, depends on the zkVM (if supports continuations) +**sequential** | linear in #{tx proofs} | constant | composable (different provers) | by design + +The sequential (IVC) strategy is an example of proof-carrying data computation. PCD-based aggregation can be distributed across mutually _untrusted_ nodes, and proofs to be aggregated arbitrarily grouped and arranged in different transcripts. + +**[TODO] Parallel proving at the ARM level.** It is possible with tree-like transcripts. Currently not supported, but [planned](https://github.com/anoma/arm-risc0/issues/112). \ No newline at end of file diff --git a/arm/Cargo.toml b/arm/Cargo.toml index 146c04fb..0a7cc243 100644 --- a/arm/Cargo.toml +++ b/arm/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" # If you want to try (experimental) std support, add `features = [ "std" ]` to risc0-zkvm risc0-zkvm = { version = "3.0.3", features = ["std", "unstable"], default-features = false } serde = { version = "1.0.197", default-features = false } +serde_with = "3.14.1" k256 = { version = "=0.13.3", features = ["arithmetic", "serde", "expose-field", "std", "ecdsa", "hash2curve"], default-features = false } sha3 = { version = "0.10", optional = true } rand = "0.8" @@ -36,3 +37,7 @@ client = ["risc0-zkvm/client"] cuda = ["risc0-zkvm/cuda"] test_circuit = [] test = ["transaction", "test_circuit"] +aggregation = ["aggregation_circuit","transaction"] +aggregation_circuit = [] +fast_aggregation = ["aggregation"] +groth16_aggregation = ["aggregation"] diff --git a/arm/elfs/batch_aggregation.bin b/arm/elfs/batch_aggregation.bin new file mode 100644 index 00000000..cb1586c0 Binary files /dev/null and b/arm/elfs/batch_aggregation.bin differ diff --git a/arm/elfs/sequential_aggregation.bin b/arm/elfs/sequential_aggregation.bin new file mode 100644 index 00000000..69db0302 Binary files /dev/null and b/arm/elfs/sequential_aggregation.bin differ diff --git a/arm/src/action.rs b/arm/src/action.rs index f5d15526..4fd0ef1c 100644 --- a/arm/src/action.rs +++ b/arm/src/action.rs @@ -38,10 +38,8 @@ impl Action { &self.logic_verifier_inputs } - pub fn verify(self) -> Result<(), ArmError> { - for unit in &self.compliance_units { - unit.verify()?; - } + pub(crate) fn get_logic_verifiers(&self) -> Result, ArmError> { + let mut logic_verifiers = Vec::new(); let compliance_intances = self .compliance_units @@ -61,7 +59,7 @@ impl Action { let action_tree = MerkleTree::from(tags.clone()); let root = action_tree.root(); - for input in self.logic_verifier_inputs { + for input in self.logic_verifier_inputs.iter() { if let Some(index) = tags.iter().position(|tag| *tag == input.tag) { if input.verifying_key != logics[index] { // The verifying_key doesn't match the resource logic @@ -69,13 +67,26 @@ impl Action { } let is_comsumed = index % 2 == 0; - let verifier = input.to_logic_verifier(is_comsumed, root)?; - verifier.verify()?; + let verifier = input.clone().to_logic_verifier(is_comsumed, root)?; + logic_verifiers.push(verifier); } else { return Err(ArmError::TagNotFound); } } + Ok(logic_verifiers) + } + + pub 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.iter() { + verifier.verify()?; + } + Ok(()) } diff --git a/arm/src/aggregation/batch.rs b/arm/src/aggregation/batch.rs new file mode 100644 index 00000000..7e792f72 --- /dev/null +++ b/arm/src/aggregation/batch.rs @@ -0,0 +1,150 @@ +use risc0_zkvm::{default_prover, Digest, ExecutorEnv, Receipt, VerifierContext}; +use risc0_zkvm::{InnerReceipt, ProverOpts}; +use serde::{Deserialize, Serialize}; + +use crate::aggregation::{ + constants::{BATCH_AGGREGATION_PK, BATCH_AGGREGATION_VK}, + BatchCU, BatchLP, +}; +use crate::compliance::ComplianceInstanceWords; +use crate::constants::COMPLIANCE_VK; +use crate::error::ArmError; +use crate::transaction::Transaction; +use crate::utils::{bytes_to_words, words_to_bytes}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BatchProof(pub InnerReceipt); + +/// Aggregates base proofs in batches. +pub struct BatchAggregation; + +impl BatchAggregation { + pub fn prove_transaction_aggregation(tx: &Transaction) -> Result { + // Collect instances, proofs, and keys. + let BatchCU { + instances: cu_instances, + receipts: cu_receipts, + } = tx.get_batch_cu(); + + let BatchLP { + instances: lp_instances, + keys: lp_keys, + receipts: lp_receipts, + } = tx.get_batch_lp()?; + + let mut cu_instances_u32: Vec = Vec::new(); + for ci in cu_instances.iter() { + cu_instances_u32.push(ComplianceInstanceWords { + u32_words: bytes_to_words(ci).try_into().map_err(|_| { + ArmError::ProveFailed( + "Error converting compliance instance into fixed-size u32 words".into(), + ) + })?, + }); + } + let lp_instances_u32: Vec> = lp_instances + .iter() + .map(|bytes| bytes_to_words(bytes)) + .collect(); + + let mut env_builder = ExecutorEnv::builder(); + + // Add proofs as assumptions + if let (Some(cu_receipts), Some(lp_receipts)) = (cu_receipts, lp_receipts) { + for receipt in cu_receipts.into_iter().chain(lp_receipts.into_iter()) { + env_builder.add_assumption(receipt); + } + } else { + return Err(ArmError::ProofVerificationFailed( + "Cannot aggregate: missing individual proof(s)".into(), + )); + } + + // Write instances and keys to guest input. + let compliance_key: Digest = *COMPLIANCE_VK; + let env = env_builder + .write(&cu_instances_u32) + .map_err(|_| ArmError::WriteWitnessFailed)? + .write(&compliance_key) + .map_err(|_| ArmError::WriteWitnessFailed)? + .write(&lp_instances_u32) + .map_err(|_| ArmError::WriteWitnessFailed)? + .write(&lp_keys) + .map_err(|_| ArmError::WriteWitnessFailed)? + .build() + .map_err(|_| ArmError::BuildProverEnvFailed)?; + + #[cfg(feature = "fast_aggregation")] + let prover_opts = ProverOpts::fast(); + + #[cfg(all(not(feature = "fast_aggregation"), feature = "groth16_aggregation"))] + let prover_opts = ProverOpts::groth16(); + + #[cfg(all( + not(feature = "fast_aggregation"), + not(feature = "groth16_aggregation") + ))] + let prover_opts = ProverOpts::succinct(); + + let prover = default_prover(); + + // Prove batch. + let receipt = prover + .prove_with_ctx( + env, + &VerifierContext::default(), + BATCH_AGGREGATION_PK, + &prover_opts, + ) + .map_err(|err| ArmError::ProveFailed(format!("Proof generation failed: {}", err)))? + .receipt; + + Ok(BatchProof(receipt.inner)) + } + + pub fn verify_transaction_aggregation( + tx: &Transaction, + proof: &BatchProof, + ) -> Result<(), ArmError> { + // Form the batch instance. + let BatchCU { + instances: compliance_instances, + receipts: _, + } = tx.get_batch_cu(); + let BatchLP { + instances: logic_instances, + keys: logic_keys, + receipts: _, + } = tx.get_batch_lp()?; + + let mut compliance_instances_u32: Vec = Vec::new(); + for ci in compliance_instances.iter() { + compliance_instances_u32.push(ComplianceInstanceWords { + u32_words: bytes_to_words(ci).try_into().map_err(|_| { + ArmError::ProofVerificationFailed( + "Error converting compliance instance into fixed-size u32 words".into(), + ) + })?, + }); + } + let logic_instances_u32: Vec> = logic_instances + .iter() + .map(|bytes| bytes_to_words(bytes)) + .collect(); + + let batch_instance = risc0_zkvm::serde::to_vec(&( + compliance_instances_u32, + *COMPLIANCE_VK, + logic_instances_u32, + logic_keys, + )) + .map_err(|_| ArmError::InstanceSerializationFailed)?; + + // Verify proof on the batch instance. + let receipt = Receipt::new(proof.0.clone(), words_to_bytes(&batch_instance).to_vec()); + + receipt.verify(*BATCH_AGGREGATION_VK).map_err(|err| { + ArmError::ProofVerificationFailed(format!("Proof verification failed: {}", err)) + }) + } +} diff --git a/arm/src/aggregation/constants.rs b/arm/src/aggregation/constants.rs new file mode 100644 index 00000000..38ac7128 --- /dev/null +++ b/arm/src/aggregation/constants.rs @@ -0,0 +1,18 @@ +use hex::FromHex; +use lazy_static::lazy_static; +use risc0_zkvm::Digest; + +// Sequential aggregation proving key / sequential aggregation guest ELF binary +pub const SEQUENTIAL_AGGREGATION_PK: &[u8] = + include_bytes!("../../elfs/sequential_aggregation.bin"); +// Batch aggregation proving key / batch aggregation guest ELF binary +pub const BATCH_AGGREGATION_PK: &[u8] = include_bytes!("../../elfs/batch_aggregation.bin"); + +lazy_static! { + // Sequential aggregation verification key / sequential aggregation image id. + pub static ref SEQUENTIAL_AGGREGATION_VK: Digest = + Digest::from_hex("4bca2e7a5569e148f6dcf933d78b3fbe52712bb7a8a0b1e2a189111a4f54c8f6").unwrap(); + + // Batch aggregation verification key / Batch aggregation image id. + pub static ref BATCH_AGGREGATION_VK: Digest = Digest::from_hex("a9c9a22ee9df47fd35ed2b319cd272c3f328a171ea04dab5e44ee15e33f5b9ce").unwrap(); +} diff --git a/arm/src/aggregation/mod.rs b/arm/src/aggregation/mod.rs new file mode 100644 index 00000000..b81821c2 --- /dev/null +++ b/arm/src/aggregation/mod.rs @@ -0,0 +1,160 @@ +use risc0_zkvm::{Digest, InnerReceipt, Receipt}; +use serde::{Deserialize, Serialize}; + +use crate::{ + aggregation::{batch::BatchProof, pcd::PcdProof}, + compliance_unit::ComplianceUnit, + error::ArmError, + logic_proof::LogicVerifier, + transaction::Transaction, +}; + +pub mod batch; +pub mod constants; +pub mod pcd; +pub mod sequential; + +/// Supported strategies to aggregate. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum AggregationStrategy { + Sequential, + Batch, +} + +/// Aggregation proof discriminating by strategies. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum AggregationProof { + /// A PCD-based aggregation proof generated with a sequential transcript. + Sequential(PcdProof), + /// A batch aggregation proof. + Batch(BatchProof), +} + +/// Holds the compliance instances, and compliance proofs (if all present) +/// of a transaction. +#[derive(Debug, Clone)] +pub(crate) struct BatchCU { + pub(crate) instances: Vec>, + pub(crate) receipts: Option>, +} + +/// Holds resource logic instances, keys, and proofs (if all present). +#[derive(Debug, Clone)] +pub(crate) struct BatchLP { + pub(crate) instances: Vec>, + pub(crate) keys: Vec, + pub(crate) receipts: Option>, +} + +impl Transaction { + fn get_batch_cu(&self) -> BatchCU { + let cus: Vec = self + .actions + .iter() + .flat_map(|a| a.get_compliance_units().clone()) + .collect(); + + let cu_instances: Vec> = cus.iter().map(|cu| cu.instance.clone()).collect(); + + let inner_receipts: Option> = if self.base_proofs_are_empty() { + None + } else { + let inner_receipts: Vec> = cus + .iter() + .map(|cu| { + let inner: Result = + bincode::deserialize(&cu.proof.clone().unwrap()); + inner + }) + .collect(); + let ir: Result, _> = inner_receipts.into_iter().collect(); + ir.ok() + }; + + match inner_receipts { + None => BatchCU { + instances: cu_instances, + receipts: None, + }, + Some(ir_vec) => { + let r: Vec = ir_vec + .into_iter() + .zip(cu_instances.clone()) + .map(|(ir, i)| Receipt::new(ir, i)) + .collect(); + + BatchCU { + instances: cu_instances, + receipts: Some(r), + } + } + } + } + + fn get_batch_lp(&self) -> Result { + let mut lps: Vec = Vec::new(); + + for action in self.actions.iter() { + let mut lp_vec: Vec = action.get_logic_verifiers()?; + lps.append(&mut lp_vec); + } + + let logic_instances: Vec> = lps.iter().map(|lp| lp.instance.clone()).collect(); + + let inner_receipts: Option> = if self.base_proofs_are_empty() { + None + } else { + let inner_receipts: Vec> = lps + .iter() + .map(|lp| { + let inner: Result = + bincode::deserialize(&lp.proof.clone().unwrap()); + inner + }) + .collect(); + let ir: Result, _> = inner_receipts.into_iter().collect(); + ir.ok() + }; + + let keys = lps.into_iter().map(|lp| lp.verifying_key).collect(); + + match inner_receipts { + None => Ok(BatchLP { + instances: logic_instances, + keys, + receipts: None, + }), + Some(ir_vec) => { + let r: Vec = ir_vec + .into_iter() + .zip(logic_instances.clone()) + .map(|(ir, i)| Receipt::new(ir, i)) + .collect(); + + let batch_lp = BatchLP { + instances: logic_instances, + keys, + receipts: Some(r), + }; + Ok(batch_lp) + } + } + } + + /// Returns `true` if any compliance or resource logic proof is `None`. + 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/src/aggregation/pcd.rs b/arm/src/aggregation/pcd.rs new file mode 100644 index 00000000..ce8f29b3 --- /dev/null +++ b/arm/src/aggregation/pcd.rs @@ -0,0 +1,268 @@ +use crate::aggregation::{BatchCU, BatchLP}; +use crate::constants::COMPLIANCE_VK; +use crate::error::ArmError; +use crate::proving_system; +use crate::utils::words_to_bytes; +use crate::{compliance::ComplianceInstance, logic_instance::LogicInstance}; +use risc0_zkvm::{ + default_prover, Digest, ExecutorEnv, InnerReceipt, ProverOpts, Receipt, VerifierContext, +}; +use serde::{Deserialize, Serialize}; + +/// A trait to aggregate proofs across nodes. +/// +/// In PCD terminology the [StepInstance] and [StepProof] are +/// the local data of each node. The local data and the input +/// [PcdMessage]s form the witness of the aggregation program. +pub trait PCDAggregation { + /// Number of input aggregations (and proofs to verify) at each step. + const INPUT_ARITY: usize; + + /// Returns the ELF of the aggregation program. + fn proving_key() -> &'static [u8]; + + /// Returns (the words of) the image id of the aggregation program. + fn verifying_key() -> Digest; + + /// Computes the output message from the input messages and the local data. + fn aggregate_step( + input_aggregations: &[PcdMessage], + step_instance: &StepInstance, + ) -> PcdMessage { + assert_eq!( + input_aggregations.len(), + ::INPUT_ARITY, + "incorrect number of input aggregations" + ); + + let mut h_in_vec = Vec::new(); + let mut d_in_vec = Vec::new(); + for aggregation in input_aggregations { + h_in_vec.push(aggregation.step_output_digest); + d_in_vec.push(aggregation.step_program_digest); + } + let h_out = crate::hash::commit_step_output_with_sha(&h_in_vec, &step_instance.output); + let d_out = crate::hash::commit_step_program_with_sha(&d_in_vec, &step_instance.program); + + PcdMessage { + step_output_digest: h_out, + step_program_digest: d_out, + } + } + + /// Proves correctness of [PCDAggregation::aggregate_step] and + /// verifies the input [PcdProof]s and [StepProof]. + fn prove_step( + input_aggregations: &[PcdMessage], + input_proofs: &[PcdProof], + step_instance: &StepInstance, + step_proof: &StepProof, + output_node: bool, + ) -> Result { + // Sanity check + if input_aggregations.len() != ::INPUT_ARITY { + return Err(ArmError::ProveFailed( + "Pcd prove failed: incorrect number of input aggregations".into(), + )); + } + + let mut base_case = true; + if !input_proofs.is_empty() { + // If not base case + if input_proofs.len() != ::INPUT_ARITY { + return Err(ArmError::ProveFailed( + "Pcd prove failed: incorrect number of input proofs".into(), + )); + } + + base_case = false; + } + + let mut env_builder = ExecutorEnv::builder(); + + // Add all proofs as assumptions. + env_builder.add_assumption(step_proof.0.clone()); + if !base_case { + for ip in input_proofs { + env_builder.add_assumption(ip.0.clone()); + } + } + + // Write all inputs. Make sure the corresponding guest circuit + // reads in the same order. + env_builder + .write(&::verifying_key()) + .map_err(|_| ArmError::WriteWitnessFailed)?; + for aggregation in input_aggregations { + env_builder + .write(&( + aggregation.step_output_digest, + aggregation.step_program_digest, + )) + .map_err(|_| ArmError::WriteWitnessFailed)?; + } + + let env = env_builder + .write(&(&step_instance.program, &step_instance.output.clone())) + .map_err(|_| ArmError::WriteWitnessFailed)? + .build() + .map_err(|_| ArmError::BuildProverEnvFailed)?; + + // If not an output node, prove fast. + let prover_opts = if output_node { + #[cfg(feature = "fast_aggregation")] + { + ProverOpts::fast() + } + + #[cfg(all(not(feature = "fast_aggregation"), feature = "groth16_aggregation"))] + { + ProverOpts::groth16() + } + + #[cfg(all( + not(feature = "fast_aggregation"), + not(feature = "groth16_aggregation") + ))] + { + ProverOpts::succinct() + } + } else { + ProverOpts::fast() + }; + + let prover = default_prover(); + + // Prove step. + let next_receipt = prover + .prove_with_ctx( + env, + &VerifierContext::default(), + ::proving_key(), + &prover_opts, + ) + .map_err(|err| ArmError::ProveFailed(format!("Pcd proof generation failed: {}", err)))? + .receipt; + + Ok(PcdProof(next_receipt.inner)) + } + + /// Verifies validity of a step aggregation. + fn verify_step(aggregation: &PcdMessage, proof: &PcdProof) -> Result<(), ArmError> { + // Form the instance. + let ag_program_key = ::verifying_key(); + let h_out = aggregation.step_output_digest; + let d_out = aggregation.step_program_digest; + let pcd_instance = risc0_zkvm::serde::to_vec(&(ag_program_key, h_out, d_out)) + .map_err(|_| ArmError::InstanceSerializationFailed)?; + + // Verify the receipt. + let receipt = Receipt::new(proof.0.clone(), words_to_bytes(&pcd_instance).to_vec()); + receipt.verify(ag_program_key).map_err(|err| { + ArmError::ProofVerificationFailed(format!("Pcd proof verification failed: {}", err)) + }) + } +} + +/// The input and output of an aggregation node (step). +#[derive(PartialEq, Debug, Clone)] +pub struct PcdMessage { + /// A binding commitment to all verified step outputs + /// that this aggregation attests to. + pub step_output_digest: Digest, + + /// A binding commitment to all verified step programs + /// that this aggregation attests to. + pub step_program_digest: Digest, +} + +impl Default for PcdMessage { + fn default() -> Self { + PcdMessage { + step_output_digest: Digest::ZERO, + step_program_digest: Digest::ZERO, + } + } +} + +/// The output and program to verify at each aggregation node (step). +/// Thus, an instance of the 'universal relation' that a zkVM realize. +#[derive(Debug, Clone)] +pub struct StepInstance { + /// The image id (aka verifying key) of the program to verify + /// at an aggregation node (step). + pub program: Digest, + + /// The serialized words of the program's output. + pub output: Vec, +} + +impl StepInstance { + /// Generic constructor. + pub fn new(output: I, image_id: VK) -> Result + where + I: serde::Serialize, + VK: Into, + { + Ok(StepInstance { + program: image_id.into(), + output: risc0_zkvm::serde::to_vec(&output).map_err(|_| ArmError::SerializationError)?, + }) + } +} + +impl TryFrom for Vec { + type Error = ArmError; + + fn try_from(value: BatchCU) -> Result { + let mut step_instances = Vec::new(); + for cu_instance in value.instances.into_iter() { + let output: ComplianceInstance = proving_system::journal_to_instance(&cu_instance)?; + let image_id = *COMPLIANCE_VK; + step_instances.push(StepInstance::new(output, image_id)?); + } + Ok(step_instances) + } +} + +impl TryFrom for Vec { + type Error = ArmError; + + fn try_from(value: BatchLP) -> Result { + let mut step_instances = Vec::new(); + for (lp_instance, lp_key) in value.instances.into_iter().zip(value.keys.into_iter()) { + let output: LogicInstance = proving_system::journal_to_instance(&lp_instance)?; + let image_id = lp_key; + step_instances.push(StepInstance::new(output, image_id)?); + } + Ok(step_instances) + } +} + +/// A proof attesting to the correctness of an [PcdMessage]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PcdProof(pub InnerReceipt); + +/// A proof attesting to the correctness of a [StepInstance]. +#[derive(Debug)] +pub struct StepProof(pub Receipt); + +impl TryFrom for Vec { + type Error = ArmError; + fn try_from(value: BatchCU) -> Result { + match value.receipts.clone() { + None => Err(ArmError::ProveFailed("missing step proofs".into())), + Some(receipts) => Ok(receipts.iter().map(|sp| StepProof(sp.clone())).collect()), + } + } +} + +impl TryFrom for Vec { + type Error = ArmError; + fn try_from(value: BatchLP) -> Result { + match value.receipts { + None => Err(ArmError::ProveFailed("missing step proofs".into())), + Some(receipts) => Ok(receipts.iter().map(|sp| StepProof(sp.clone())).collect()), + } + } +} diff --git a/arm/src/aggregation/sequential.rs b/arm/src/aggregation/sequential.rs new file mode 100644 index 00000000..db23717d --- /dev/null +++ b/arm/src/aggregation/sequential.rs @@ -0,0 +1,126 @@ +use risc0_zkvm::Digest; + +use crate::{ + aggregation::constants::{SEQUENTIAL_AGGREGATION_PK, SEQUENTIAL_AGGREGATION_VK}, + error::ArmError, + transaction::Transaction, +}; + +use super::pcd::{PCDAggregation, PcdMessage, PcdProof, StepInstance, StepProof}; + +pub struct SequentialAggregation; + +impl SequentialAggregation { + /// Aggregate instances in a transcript. + pub fn aggregate_transcript(instances: &[StepInstance]) -> PcdMessage { + let mut aggregated_instances = PcdMessage::default(); + + for instance in instances { + aggregated_instances = ::aggregate_step( + &[aggregated_instances], + instance, + ); + } + + aggregated_instances + } + + /// Prove correctness of an aggregation transcript. + pub fn prove_transcript_aggregation( + instances: &[StepInstance], + proofs: &[StepProof], + ) -> Result { + if instances.len() != proofs.len() { + // Can't aggregate. + return Err(ArmError::ProveFailed( + "Mismatch in number of individual instances and proofs".into(), + )); + } + + let mut agg = PcdMessage::default(); + let mut agg_proof = Vec::new(); + for (pos, (instance, proof)) in instances.iter().zip(proofs.iter()).enumerate() { + let proof_out = ::prove_step( + &[agg.clone()], + &agg_proof, + instance, + proof, + pos == instances.len() - 1, + )?; + + agg = ::aggregate_step(&[agg], instance); + agg_proof = vec![proof_out]; + } + agg_proof + .pop() + .ok_or(ArmError::ProveFailed("Error popping out pcd proof".into())) + } + + /// Prove correctness of the transcript induced by a transaction. + pub fn prove_transaction_aggregation(tx: &Transaction) -> Result { + if let (instances, Some(proofs)) = &SequentialAggregation::transaction_transcript(tx)? { + SequentialAggregation::prove_transcript_aggregation(instances, proofs) + } else { + Err(ArmError::ProveFailed( + "Error deriving transcript for proving (individual instances and proofs)".into(), + )) + } + } + + /// Verifies the correctness of a transcript aggregation. + pub fn verify_transcript_aggregation( + instances: &[StepInstance], + proof: &PcdProof, + ) -> Result<(), ArmError> { + let aggregation = SequentialAggregation::aggregate_transcript(instances); + + // Verify last PCD step for the aggregated instance and proof. + ::verify_step(&aggregation, proof) + } + + /// Verifies the correctness of the transcript induced by the transaction. + pub fn verify_transaction_aggregation( + tx: &Transaction, + proof: &PcdProof, + ) -> Result<(), ArmError> { + let (instances, _) = SequentialAggregation::transaction_transcript(tx)?; + SequentialAggregation::verify_transcript_aggregation(&instances, proof) + } + + /// Derives the transcript induced by the transaction. + pub fn transaction_transcript( + tx: &Transaction, + ) -> Result<(Vec, Option>), ArmError> { + let batch_cu = tx.get_batch_cu(); + let batch_lp = tx.get_batch_lp()?; + let mut step_instances: Vec = batch_cu.clone().try_into()?; + step_instances.append( + &mut >>::try_into(batch_lp.clone())?, + ); + + let sp_cu: Option> = batch_cu.try_into().ok(); + let sp_lp: Option> = batch_lp.try_into().ok(); + + // We need the step proofs to prove aggregation, but not to verify the aggregation. + let mut step_proofs: Option> = None; + if let (Some(sp_cu), Some(mut sp_lp)) = (sp_cu, sp_lp) { + let mut sp = sp_cu; + sp.append(&mut sp_lp); + step_proofs = Some(sp); + }; + + Ok((step_instances, step_proofs)) + } +} + +impl PCDAggregation for SequentialAggregation { + const INPUT_ARITY: usize = 1; + + fn proving_key() -> &'static [u8] { + SEQUENTIAL_AGGREGATION_PK + } + + fn verifying_key() -> Digest { + *SEQUENTIAL_AGGREGATION_VK + } +} diff --git a/arm/src/compliance.rs b/arm/src/compliance.rs index 44e332f5..0b293366 100644 --- a/arm/src/compliance.rs +++ b/arm/src/compliance.rs @@ -1,3 +1,6 @@ +// Size hard-coded to two resources per unit +const COMPLIANCE_INSTANCE_SIZE: usize = 56; + use crate::{ error::ArmError, merkle_path::MerklePath, @@ -15,6 +18,7 @@ use k256::{ }; use lazy_static::lazy_static; use risc0_zkvm::Digest; +use serde_with::serde_as; lazy_static! { pub static ref INITIAL_ROOT: Digest = Digest::from_hex("cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06") @@ -33,6 +37,13 @@ pub struct ComplianceInstance { pub delta_y: [u32; 8], } +#[serde_as] +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ComplianceInstanceWords { + #[serde_as(as = "[_; COMPLIANCE_INSTANCE_SIZE]")] + pub u32_words: [u32; COMPLIANCE_INSTANCE_SIZE], +} + #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct ComplianceWitness { /// The consumed resource diff --git a/arm/src/compliance_unit.rs b/arm/src/compliance_unit.rs index fb35132a..f9cbbace 100644 --- a/arm/src/compliance_unit.rs +++ b/arm/src/compliance_unit.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ComplianceUnit { // vk is a constant in the compliance unit, so we don't place it here. - pub proof: Vec, + pub proof: Option>, pub instance: Vec, } @@ -19,11 +19,20 @@ impl ComplianceUnit { // them as parameters. Instance is generated by proving. pub fn create(witness: &ComplianceWitness) -> Result { let (proof, instance) = prove(COMPLIANCE_PK, witness)?; - Ok(ComplianceUnit { proof, instance }) + Ok(ComplianceUnit { + proof: Some(proof), + instance, + }) } pub fn verify(&self) -> Result<(), ArmError> { - verify_proof(&COMPLIANCE_VK, &self.instance, &self.proof) + if let Some(proof) = &self.proof { + verify_proof(&COMPLIANCE_VK, &self.instance, proof) + } else { + Err(ArmError::ProofVerificationFailed( + "Missing compliance proof".into(), + )) + } } pub fn delta(&self) -> Result { diff --git a/arm/src/hash.rs b/arm/src/hash.rs new file mode 100644 index 00000000..b1fddc39 --- /dev/null +++ b/arm/src/hash.rs @@ -0,0 +1,36 @@ +use risc0_zkvm::sha::{Impl, Sha256, DIGEST_BYTES}; +use risc0_zkvm::Digest; + +use crate::utils::words_to_bytes; + +pub fn commit_step_output_with_sha(prev_hashes: &[Digest], step_output_serde: &[u32]) -> Digest { + sha_digest( + &flatten_prev_hashes(prev_hashes), + words_to_bytes(step_output_serde), + ) +} + +pub fn commit_step_program_with_sha(prev_hashes: &[Digest], step_program: &Digest) -> Digest { + sha_digest(&flatten_prev_hashes(prev_hashes), step_program.as_bytes()) +} + +fn sha_digest(prev_hashes: &[u8], tip: &[u8]) -> Digest { + let prev_hashes_length: usize = prev_hashes.len(); + let tip_length: usize = tip.len(); + let mut bytes = vec![0u8; tip_length + prev_hashes_length]; + bytes[0..tip_length].clone_from_slice(tip); + bytes[tip_length..tip_length + prev_hashes_length].clone_from_slice(prev_hashes); + + *Impl::hash_bytes(&bytes) +} + +fn flatten_prev_hashes(prev_hashes: &[Digest]) -> Vec { + let mut prev_hashes_bytes = vec![0u8; DIGEST_BYTES * prev_hashes.len()]; + let mut offset: usize = 0; + for h in prev_hashes { + prev_hashes_bytes[offset..offset + DIGEST_BYTES].clone_from_slice(h.as_bytes()); + offset += DIGEST_BYTES; + } + + prev_hashes_bytes +} diff --git a/arm/src/lib.rs b/arm/src/lib.rs index 8b9cf7d1..d1f83821 100644 --- a/arm/src/lib.rs +++ b/arm/src/lib.rs @@ -1,8 +1,10 @@ #[cfg(feature = "transaction")] pub mod action; pub mod action_tree; +#[cfg(feature = "aggregation")] +pub mod aggregation; pub mod authorization; -#[cfg(feature = "compliance_circuit")] +#[cfg(any(feature = "compliance_circuit", feature = "aggregation_circuit"))] pub mod compliance; #[cfg(feature = "transaction")] pub mod compliance_unit; @@ -13,6 +15,8 @@ pub mod delta_proof; pub mod encryption; pub mod error; pub mod evm; +#[cfg(feature = "aggregation_circuit")] +pub mod hash; pub mod logic_instance; #[cfg(feature = "transaction")] pub mod logic_proof; diff --git a/arm/src/logic_proof.rs b/arm/src/logic_proof.rs index b55dd470..cb2f7023 100644 --- a/arm/src/logic_proof.rs +++ b/arm/src/logic_proof.rs @@ -30,7 +30,7 @@ pub trait LogicProver: Default + Clone + Serialize + for<'de> Deserialize<'de> { fn prove(&self) -> Result { let (proof, instance) = prove(Self::proving_key(), self.witness())?; Ok(LogicVerifier { - proof, + proof: Some(proof), instance, verifying_key: Self::verifying_key(), }) @@ -39,7 +39,7 @@ pub trait LogicProver: Default + Clone + Serialize + for<'de> Deserialize<'de> { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LogicVerifier { - pub proof: Vec, + pub proof: Option>, pub instance: Vec, pub verifying_key: Digest, } @@ -49,13 +49,19 @@ pub struct LogicVerifierInputs { pub tag: Digest, pub verifying_key: Digest, pub app_data: AppData, - pub proof: Vec, + pub proof: Option>, } impl LogicVerifier { pub fn verify(&self) -> Result<(), ArmError> { - verify_proof(&self.verifying_key, &self.instance, &self.proof) - .map_err(|err| ArmError::ProofVerificationFailed(err.to_string())) + if let Some(proof) = &self.proof { + verify_proof(&self.verifying_key, &self.instance, proof) + .map_err(|err| ArmError::ProofVerificationFailed(err.to_string())) + } else { + Err(ArmError::ProofVerificationFailed( + "Missing logic proof".into(), + )) + } } pub fn get_instance(&self) -> Result { diff --git a/arm/src/tests.rs b/arm/src/tests.rs index b5d4e02d..72beac3d 100644 --- a/arm/src/tests.rs +++ b/arm/src/tests.rs @@ -151,3 +151,63 @@ fn test_action() { fn test_transaction() { let _ = generate_test_transaction(1); } + +#[test] +#[cfg(feature = "aggregation")] +fn test_aggregation_works() { + use crate::aggregation::AggregationStrategy; + + let tx = generate_test_transaction(1); + + for strategy in [AggregationStrategy::Sequential, AggregationStrategy::Batch] { + let mut tx_str = tx.clone(); + assert!(tx_str.aggregate_with_strategy(strategy.clone()).is_ok()); + assert!(tx_str.aggregation_proof.is_some()); + assert!(tx_str.verify_aggregation().is_ok()); + } +} + +#[test] +#[cfg(feature = "aggregation")] +fn test_verify_aggregation_fails_for_incorrect_instances() { + use crate::aggregation::AggregationStrategy; + + let tx = generate_test_transaction(2); + + for strategy in [AggregationStrategy::Sequential, AggregationStrategy::Batch] { + let mut tx_str = tx.clone(); + assert!(tx_str.aggregate_with_strategy(strategy).is_ok()); + + tx_str.actions[0].logic_verifier_inputs.pop(); + + assert!(tx_str.verify_aggregation().is_err()); + } +} + +#[test] +#[cfg(feature = "aggregation")] +fn test_cannot_aggregate_invalid_proofs() { + use crate::{aggregation::AggregationStrategy, logic_proof::LogicVerifierInputs}; + + let tx = generate_test_transaction(2); + + // 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. + tag: tx.actions[0].logic_verifier_inputs[0].tag, + app_data: tx.actions[0].logic_verifier_inputs[0].app_data.clone(), + }; + + let bad_action = Action { + compliance_units: tx.actions[0].clone().compliance_units, + logic_verifier_inputs: vec![bad_lproof], + }; + let bad_tx = Transaction::create(vec![bad_action, tx.actions[1].clone()], tx.delta_proof); + + for strategy in [AggregationStrategy::Sequential, AggregationStrategy::Batch] { + let mut bad_tx_str = bad_tx.clone(); + assert!(bad_tx_str.aggregate_with_strategy(strategy).is_err()); + assert!(bad_tx_str.aggregation_proof.is_none()); + } +} diff --git a/arm/src/transaction.rs b/arm/src/transaction.rs index 74ed9958..8a31e096 100644 --- a/arm/src/transaction.rs +++ b/arm/src/transaction.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "aggregation")] +use crate::aggregation::{ + batch::BatchAggregation, sequential::SequentialAggregation, AggregationProof, + AggregationStrategy, +}; use crate::{ action::Action, delta_proof::{DeltaInstance, DeltaProof, DeltaWitness}, @@ -13,6 +18,8 @@ pub struct Transaction { pub delta_proof: Delta, // We can't support unbalanced transactions, so this is just a placeholder. pub expected_balance: Option>, + // If present, attests to the validity of all individual proofs. + pub aggregation_proof: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -30,6 +37,7 @@ impl Transaction { actions, delta_proof: delta, expected_balance: None, + aggregation_proof: None, } } @@ -43,6 +51,7 @@ impl Transaction { actions: self.actions, delta_proof, expected_balance: self.expected_balance, + aggregation_proof: self.aggregation_proof, }) } Delta::Proof(_) => Ok(self), @@ -94,3 +103,77 @@ impl Transaction { Transaction::create(actions, delta) } } + +#[cfg(feature = "aggregation")] +impl Transaction { + /// Aggregates all the transaction proofs with the default strategy. + pub fn aggregate(&mut self) -> Result<(), ArmError> { + self.aggregate_with_strategy(AggregationStrategy::Batch) + } + + /// Aggregates all the transaction proofs using the passed aggregation strategy. + /// 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_with_strategy( + &mut self, + strategy: AggregationStrategy, + ) -> Result<(), ArmError> { + let agg_proof = match strategy { + AggregationStrategy::Sequential => { + SequentialAggregation::prove_transaction_aggregation(self) + .map(AggregationProof::Sequential)? + } + AggregationStrategy::Batch => BatchAggregation::prove_transaction_aggregation(self) + .map(AggregationProof::Batch)?, + }; + + self.aggregation_proof = + Some(bincode::serialize(&agg_proof).map_err(|_| ArmError::SerializationError)?); + + self.erase_base_proofs(); + Ok(()) + } + + /// Verifies the aggregated proof of the transaction. + pub fn verify_aggregation(&self) -> Result<(), ArmError> { + if let Some(agg_proof) = &self.aggregation_proof { + match bincode::deserialize(agg_proof) + .map_err(|_| ArmError::InnerReceiptDeserializationError)? + { + AggregationProof::Sequential(proof) => { + SequentialAggregation::verify_transaction_aggregation(self, &proof) + } + AggregationProof::Batch(proof) => { + BatchAggregation::verify_transaction_aggregation(self, &proof) + } + } + } else { + Err(ArmError::ProofVerificationFailed( + "Missing aggregation proof".into(), + )) + } + } + + pub fn get_raw_aggregation_proof(&self) -> Option> { + if let Some(agg_proof) = &self.aggregation_proof { + match bincode::deserialize(agg_proof).unwrap() { + AggregationProof::Sequential(proof) => Some(bincode::serialize(&proof.0).unwrap()), + AggregationProof::Batch(proof) => Some(bincode::serialize(&proof.0).unwrap()), + } + } else { + None + } + } + + // 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; + } + } + } +} diff --git a/arm_circuits/Cargo.lock b/arm_circuits/Cargo.lock index 6a302f56..9b092e3e 100644 --- a/arm_circuits/Cargo.lock +++ b/arm_circuits/Cargo.lock @@ -685,6 +685,24 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "batch_aggregation" +version = "0.1.0" +dependencies = [ + "arm", + "batch_aggregation_methods", + "bincode", + "risc0-zkvm", + "serde", +] + +[[package]] +name = "batch_aggregation_methods" +version = "0.1.0" +dependencies = [ + "risc0-build", +] + [[package]] name = "bincode" version = "1.3.3" diff --git a/arm_circuits/Cargo.toml b/arm_circuits/Cargo.toml index 312787e1..8169f5c4 100644 --- a/arm_circuits/Cargo.toml +++ b/arm_circuits/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "compliance", "trivial_logic", "counter", "kudo_main", "simple_kudo_denomination", "simple_kudo_receive", "logic_test" ] +members = [ "compliance", "trivial_logic", "counter", "kudo_main", "simple_kudo_denomination", "simple_kudo_receive", "logic_test", "batch_aggregation" ] # Always optimize; otherwise tests take excessively long. [profile.dev] diff --git a/arm_circuits/batch_aggregation/Cargo.toml b/arm_circuits/batch_aggregation/Cargo.toml new file mode 100644 index 00000000..8f0a286b --- /dev/null +++ b/arm_circuits/batch_aggregation/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "batch_aggregation" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# If you want to try (experimental) std support, add `features = [ "std" ]` to risc0-zkvm +batch_aggregation_methods = { path = "methods" } +risc0-zkvm = "3.0.3" +arm = { path = "../../arm", features = ["transaction"]} +bincode = "1.3.3" +serde = "1.0.219" diff --git a/arm_circuits/batch_aggregation/README.md b/arm_circuits/batch_aggregation/README.md new file mode 100644 index 00000000..b75b0f3c --- /dev/null +++ b/arm_circuits/batch_aggregation/README.md @@ -0,0 +1,5 @@ +# Batch aggregation circuit +The [batch aggregation circuit](methods/guest/src/main.rs) does the following: + * verify the input compliance instances with the input compliance key, + * verify the input resource logic instances with the input resource logic keys, + * output all the inputs. \ No newline at end of file diff --git a/arm_circuits/batch_aggregation/methods/Cargo.toml b/arm_circuits/batch_aggregation/methods/Cargo.toml new file mode 100644 index 00000000..3063fe5b --- /dev/null +++ b/arm_circuits/batch_aggregation/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "batch_aggregation_methods" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "3.0.3", features = ["unstable"] } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/arm_circuits/batch_aggregation/methods/build.rs b/arm_circuits/batch_aggregation/methods/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/arm_circuits/batch_aggregation/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/arm_circuits/batch_aggregation/methods/guest/Cargo.toml b/arm_circuits/batch_aggregation/methods/guest/Cargo.toml new file mode 100644 index 00000000..5a782d54 --- /dev/null +++ b/arm_circuits/batch_aggregation/methods/guest/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "batch_aggregation" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "3.0.3", features = ["std", "unstable"], default-features = false } +arm = { path = "../../../../arm", features = ["aggregation_circuit"], default-features = false} \ No newline at end of file diff --git a/arm_circuits/batch_aggregation/methods/guest/src/main.rs b/arm_circuits/batch_aggregation/methods/guest/src/main.rs new file mode 100644 index 00000000..2e28ea41 --- /dev/null +++ b/arm_circuits/batch_aggregation/methods/guest/src/main.rs @@ -0,0 +1,29 @@ +use risc0_zkvm::guest::env; +use risc0_zkvm::Digest; + +use arm::compliance::ComplianceInstanceWords; + +/// The batch aggregation circuit. +fn main() { + // Read the inputs. + let compliance_instances: Vec = env::read(); + let compliance_key: Digest = env::read(); + let logic_instances: Vec> = env::read(); + let logic_keys: Vec = env::read(); // Assume same length as `logic_instances`. Else will panic. + + // Verify the proofs. + for ci in compliance_instances.iter() { + env::verify(compliance_key, &ci.u32_words).unwrap(); + } + for i in 0..logic_instances.len() { + env::verify(logic_keys[i], &logic_instances[i]).unwrap(); + } + + // The output. + env::commit(&( + compliance_instances, + compliance_key, + logic_instances, + logic_keys, + )); +} diff --git a/arm_circuits/batch_aggregation/methods/src/lib.rs b/arm_circuits/batch_aggregation/methods/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/arm_circuits/batch_aggregation/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/arm_circuits/batch_aggregation/src/main.rs b/arm_circuits/batch_aggregation/src/main.rs new file mode 100644 index 00000000..cad9109a --- /dev/null +++ b/arm_circuits/batch_aggregation/src/main.rs @@ -0,0 +1,54 @@ +use risc0_zkvm::{default_prover, Digest, ExecutorEnv, InnerReceipt, Receipt}; + +use arm::{logic_proof::LogicVerifier, transaction::generate_test_transaction}; + +use batch_aggregation_methods::{BATCH_AGGREGATION_ELF, BATCH_AGGREGATION_ID}; + +pub fn main() { + let tx = generate_test_transaction(2); + let cu_1 = tx.actions[0].get_compliance_units()[0].clone(); + let cu_2 = tx.actions[1].get_compliance_units()[0].clone(); + let lv_1: Vec = tx.actions[0].clone().try_into().unwrap(); + let lp_1 = lv_1[0].clone(); + let lv_2: Vec = tx.actions[1].clone().try_into().unwrap(); + let lp_2 = lv_2[0].clone(); + + let mut inner: InnerReceipt = bincode::deserialize(&cu_1.proof.unwrap()).unwrap(); + let cu_1_receipt = Receipt::new(inner, cu_1.instance.clone().to_vec()); + inner = bincode::deserialize(&cu_2.proof.unwrap()).unwrap(); + let cu_2_receipt = Receipt::new(inner, cu_2.instance.clone().to_vec()); + inner = bincode::deserialize(&lp_1.proof.unwrap()).unwrap(); + let lp_1_receipt = Receipt::new(inner, lp_1.instance.clone().to_vec()); + inner = bincode::deserialize(&lp_2.proof.unwrap()).unwrap(); + let lp_2_receipt = Receipt::new(inner, lp_2.instance.clone().to_vec()); + + // Test batch aggregation (both compliance and resource logic instances). + let compliance_key: Digest = *arm::constants::COMPLIANCE_VK; + let env = ExecutorEnv::builder() + .add_assumption(cu_1_receipt) + .add_assumption(cu_2_receipt) + .add_assumption(lp_1_receipt) + .add_assumption(lp_2_receipt) + .write(&vec![cu_1.instance, cu_2.instance]) + .unwrap() + .write(&compliance_key) + .unwrap() + .write(&vec![lp_1.instance, lp_2.instance]) + .unwrap() + .write(&vec![ + Digest::new(lp_1.verifying_key.try_into().unwrap()), + Digest::new(lp_2.verifying_key.try_into().unwrap()), + ]) + .unwrap() + .build() + .unwrap(); + + let batch_receipt = default_prover() + .prove(env, BATCH_AGGREGATION_ELF) + .unwrap() + .receipt; + + batch_receipt.verify(BATCH_AGGREGATION_ID).unwrap(); + + println!("Success!"); +} diff --git a/arm_circuits/sequential_aggregation/README.md b/arm_circuits/sequential_aggregation/README.md new file mode 100644 index 00000000..dbdf0c85 --- /dev/null +++ b/arm_circuits/sequential_aggregation/README.md @@ -0,0 +1,5 @@ +# Sequential aggregation circuit +The [circuit of a single step in a sequential aggregation](methods/guest/src/main.rs) does the following: + * verify the step output for the step program, + * if not the base case, verify the input aggregation, + * hash the step output and step program with the input running hash. \ No newline at end of file diff --git a/arm_circuits/sequential_aggregation/methods/Cargo.toml b/arm_circuits/sequential_aggregation/methods/Cargo.toml new file mode 100644 index 00000000..f024318f --- /dev/null +++ b/arm_circuits/sequential_aggregation/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sequential_aggregation_methods" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "3.0.3", features = ["unstable"] } + +[package.metadata.risc0] +methods = ["guest"] \ No newline at end of file diff --git a/arm_circuits/sequential_aggregation/methods/build.rs b/arm_circuits/sequential_aggregation/methods/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/arm_circuits/sequential_aggregation/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/arm_circuits/sequential_aggregation/methods/guest/Cargo.toml b/arm_circuits/sequential_aggregation/methods/guest/Cargo.toml new file mode 100644 index 00000000..d9e66849 --- /dev/null +++ b/arm_circuits/sequential_aggregation/methods/guest/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sequential_aggregation" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "3.0.3", features = ["std", "unstable"], default-features = false } +arm = { path = "../../../../arm", features = ["aggregation_circuit"], default-features = false} \ No newline at end of file diff --git a/arm_circuits/sequential_aggregation/methods/guest/src/main.rs b/arm_circuits/sequential_aggregation/methods/guest/src/main.rs new file mode 100644 index 00000000..28ccf545 --- /dev/null +++ b/arm_circuits/sequential_aggregation/methods/guest/src/main.rs @@ -0,0 +1,31 @@ +use risc0_zkvm::guest::env; +use risc0_zkvm::serde; +use risc0_zkvm::Digest; + +/// The circuit of a single step in a sequential aggregation. +fn main() { + // Read the (purported) aggregation program ID + // and input hashes. + let ag_program_key: Digest = env::read(); + let (h_in, d_in): (Digest, Digest) = env::read(); + + // Read the step program ID and (serialized) step output. + let (step_program, step_output): (Digest, Vec) = env::read(); + + // Verify the step proof. + env::verify(step_program, &step_output).unwrap(); + + // Verify the input aggregation (a purported previous execution of this program) + if h_in != Digest::ZERO && d_in != Digest::ZERO { + env::verify( + ag_program_key, + &serde::to_vec(&(ag_program_key, h_in, d_in)).unwrap(), + ) + .unwrap(); + } + + // Hash-chain the verified output and program. + let h_out = arm::hash::commit_step_output_with_sha(&[h_in], &step_output); + let d_out = arm::hash::commit_step_program_with_sha(&[d_in], &step_program); + env::commit(&(ag_program_key, h_out, d_out)); +} diff --git a/arm_circuits/sequential_aggregation/methods/src/lib.rs b/arm_circuits/sequential_aggregation/methods/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/arm_circuits/sequential_aggregation/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs"));