diff --git a/emmarin/.gitignore b/emmarin/.gitignore new file mode 100644 index 0000000..6125671 --- /dev/null +++ b/emmarin/.gitignore @@ -0,0 +1 @@ +*profile.pb \ No newline at end of file diff --git a/emmarin/cl/Cargo.toml b/emmarin/cl/Cargo.toml index 836a57c..86795ca 100644 --- a/emmarin/cl/Cargo.toml +++ b/emmarin/cl/Cargo.toml @@ -1,6 +1,14 @@ [workspace] resolver = "2" -members = [ "cl", "ledger", "ledger_proof_statements", "risc0_proofs", "ledger_validity_proof"] +members = [ + "cl", + "ledger", + "ledger_proof_statements", + "risc0_proofs", + "bundle_risc0_proof", + "ptx_risc0_proof", + "ledger_validity_proof" +] # Always optimize; building and running the risc0_proofs takes much longer without optimization. [profile.dev] diff --git a/emmarin/cl/bundle_risc0_proof/Cargo.toml b/emmarin/cl/bundle_risc0_proof/Cargo.toml new file mode 100644 index 0000000..63e3d70 --- /dev/null +++ b/emmarin/cl/bundle_risc0_proof/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nomos_cl_bundle_risc0_proof" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = ["bundle"] + diff --git a/emmarin/cl/bundle_risc0_proof/build.rs b/emmarin/cl/bundle_risc0_proof/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/emmarin/cl/bundle_risc0_proof/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/cl/risc0_proofs/balance/Cargo.toml b/emmarin/cl/bundle_risc0_proof/bundle/Cargo.toml similarity index 90% rename from emmarin/cl/risc0_proofs/balance/Cargo.toml rename to emmarin/cl/bundle_risc0_proof/bundle/Cargo.toml index 161f1c8..0903802 100644 --- a/emmarin/cl/risc0_proofs/balance/Cargo.toml +++ b/emmarin/cl/bundle_risc0_proof/bundle/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "balance" +name = "bundle" version = "0.1.0" edition = "2021" @@ -10,6 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } ledger_proof_statements = { path = "../../ledger_proof_statements" } +nomos_cl_ptx_risc0_proof = { path = "../../ptx_risc0_proof" } [patch.crates-io] diff --git a/emmarin/cl/bundle_risc0_proof/bundle/src/main.rs b/emmarin/cl/bundle_risc0_proof/bundle/src/main.rs new file mode 100644 index 0000000..0473977 --- /dev/null +++ b/emmarin/cl/bundle_risc0_proof/bundle/src/main.rs @@ -0,0 +1,49 @@ +use cl::cl::BalanceWitness; +use cl::zone_layer::notes::ZoneId; +use ledger_proof_statements::bundle::{BundlePrivate, BundlePublic, LedgerUpdate}; +use risc0_zkvm::{guest::env, serde}; +use std::collections::BTreeMap; + +fn main() { + let bundle_private: BundlePrivate = env::read(); + let bundle_id = bundle_private.id(); + + let BundlePrivate { bundle, balances } = bundle_private; + assert_eq!(bundle.len(), balances.len()); + + let mut zone_ledger_updates: BTreeMap = BTreeMap::new(); + + for (ptx_public, balance) in bundle.into_iter().zip(balances.iter()) { + assert_eq!(ptx_public.ptx.balance, balance.commit()); + env::verify( + nomos_cl_ptx_risc0_proof::PTX_ID, + &serde::to_vec(&ptx_public).unwrap(), + ) + .unwrap(); + + for (input, cm_mmr) in ptx_public.ptx.inputs.iter().zip(ptx_public.cm_mmrs) { + let zone_ledger_update = zone_ledger_updates.entry(input.zone_id).or_default(); + + zone_ledger_update.nullifiers.push(input.nullifier); + + zone_ledger_update + .cm_roots + .extend(cm_mmr.roots.iter().map(|r| r.root)); + } + + for output in &ptx_public.ptx.outputs { + zone_ledger_updates + .entry(output.zone_id) + .or_default() + .commitments + .push(output.note_comm); + } + } + + assert!(BalanceWitness::combine(balances, [0u8; 16]).is_zero()); + + env::commit(&BundlePublic { + bundle_id, + zone_ledger_updates, + }); +} diff --git a/emmarin/cl/bundle_risc0_proof/src/lib.rs b/emmarin/cl/bundle_risc0_proof/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/emmarin/cl/bundle_risc0_proof/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/emmarin/cl/cl/Cargo.toml b/emmarin/cl/cl/Cargo.toml index 761fa4a..ead4861 100644 --- a/emmarin/cl/cl/Cargo.toml +++ b/emmarin/cl/cl/Cargo.toml @@ -12,4 +12,5 @@ rand = "0.8.5" rand_core = "0.6.0" hex = "0.4.3" curve25519-dalek = {version = "4.1", features = ["serde", "digest", "rand_core"]} -sha2 = "0.10" \ No newline at end of file +sha2 = "0.10" +lazy_static = "1.5.0" \ No newline at end of file diff --git a/emmarin/cl/cl/src/cl/bundle.rs b/emmarin/cl/cl/src/cl/bundle.rs deleted file mode 100644 index aa32e08..0000000 --- a/emmarin/cl/cl/src/cl/bundle.rs +++ /dev/null @@ -1,138 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{cl::partial_tx::PartialTx, zone_layer::notes::ZoneId}; -use sha2::{Digest, Sha256}; -use std::collections::HashSet; - -/// The transaction bundle is a collection of partial transactions. -/// The goal in bundling transactions is to produce a set of partial transactions -/// that balance each other. - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct BundleId(pub [u8; 32]); - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Bundle { - pub partials: Vec, -} - -impl Bundle { - pub fn zones(&self) -> HashSet { - self.partials - .iter() - .flat_map(|ptx| { - ptx.inputs - .iter() - .map(|i| i.zone_id) - .chain(ptx.outputs.iter().map(|o| o.zone_id)) - }) - .collect() - } - - /// - pub fn id(&self) -> BundleId { - // TODO: change to merkle root - let mut hasher = Sha256::new(); - hasher.update(b"NOMOS_CL_BUNDLE_ID"); - for ptx in &self.partials { - hasher.update(&ptx.root().0); - } - - BundleId(hasher.finalize().into()) - } -} - -#[cfg(test)] -mod test { - use crate::cl::{ - balance::{BalanceWitness, UnitBalance}, - input::InputWitness, - note::{derive_unit, NoteWitness}, - nullifier::NullifierSecret, - output::OutputWitness, - partial_tx::PartialTxWitness, - }; - - #[test] - fn test_bundle_balance() { - let mut rng = rand::thread_rng(); - let zone_id = [0; 32]; - let (nmo, eth, crv) = (derive_unit("NMO"), derive_unit("ETH"), derive_unit("CRV")); - - let nf_a = NullifierSecret::random(&mut rng); - let nf_b = NullifierSecret::random(&mut rng); - let nf_c = NullifierSecret::random(&mut rng); - - let nmo_10_utxo = OutputWitness::new( - NoteWitness::basic(10, nmo, &mut rng), - nf_a.commit(), - zone_id, - ); - let nmo_10_in = InputWitness::from_output(nmo_10_utxo, nf_a); - - let eth_23_utxo = OutputWitness::new( - NoteWitness::basic(23, eth, &mut rng), - nf_b.commit(), - zone_id, - ); - let eth_23_in = InputWitness::from_output(eth_23_utxo, nf_b); - - let crv_4840_out = OutputWitness::new( - NoteWitness::basic(4840, crv, &mut rng), - nf_c.commit(), - zone_id, - ); - - let ptx_unbalanced = PartialTxWitness { - inputs: vec![nmo_10_in, eth_23_in], - outputs: vec![crv_4840_out], - balance_blinding: BalanceWitness::random_blinding(&mut rng), - }; - - assert!(!ptx_unbalanced.balance().is_zero()); - assert_eq!( - ptx_unbalanced.balance().balances, - vec![ - UnitBalance { - unit: nmo, - pos: 0, - neg: 10 - }, - UnitBalance { - unit: eth, - pos: 0, - neg: 23 - }, - UnitBalance { - unit: crv, - pos: 4840, - neg: 0 - }, - ] - ); - - let crv_4840_in = InputWitness::from_output(crv_4840_out, nf_c); - let nmo_10_out = OutputWitness::new( - NoteWitness::basic(10, nmo, &mut rng), - NullifierSecret::random(&mut rng).commit(), // transferring to a random owner - zone_id, - ); - let eth_23_out = OutputWitness::new( - NoteWitness::basic(23, eth, &mut rng), - NullifierSecret::random(&mut rng).commit(), // transferring to a random owner - zone_id, - ); - - let ptx_solved = PartialTxWitness { - inputs: vec![crv_4840_in], - outputs: vec![nmo_10_out, eth_23_out], - balance_blinding: BalanceWitness::random_blinding(&mut rng), - }; - - let bundle_balance = - BalanceWitness::combine([ptx_unbalanced.balance(), ptx_solved.balance()], [0; 16]); - - assert!(bundle_balance.is_zero()); - assert_eq!(bundle_balance.balances, vec![]); - } -} diff --git a/emmarin/cl/cl/src/cl/merkle.rs b/emmarin/cl/cl/src/cl/merkle.rs index bb10e3c..8aecb78 100644 --- a/emmarin/cl/cl/src/cl/merkle.rs +++ b/emmarin/cl/cl/src/cl/merkle.rs @@ -43,6 +43,8 @@ pub fn root(elements: [[u8; 32]; N]) -> [u8; 32] { nodes[0] } +pub type Path = Vec; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PathNode { Left([u8; 32]), @@ -66,7 +68,7 @@ pub fn path_root(leaf: [u8; 32], path: &[PathNode]) -> [u8; 32] { computed_hash } -pub fn path(leaves: [[u8; 32]; N], idx: usize) -> Vec { +pub fn path(leaves: [[u8; 32]; N], idx: usize) -> Path { assert!(N.is_power_of_two()); assert!(idx < N); diff --git a/emmarin/cl/cl/src/cl/mmr.rs b/emmarin/cl/cl/src/cl/mmr.rs index a5a4151..0a11847 100644 --- a/emmarin/cl/cl/src/cl/mmr.rs +++ b/emmarin/cl/cl/src/cl/mmr.rs @@ -2,7 +2,7 @@ use crate::cl::merkle; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct MMR { pub roots: Vec, } @@ -18,9 +18,16 @@ pub struct MMRProof { pub path: Vec, } +impl MMRProof { + pub fn root(&self, elem: &[u8]) -> [u8; 32] { + let leaf = merkle::leaf(elem); + merkle::path_root(leaf, &self.path) + } +} + impl MMR { pub fn new() -> Self { - Self { roots: vec![] } + Self::default() } pub fn push(&mut self, elem: &[u8]) -> MMRProof { @@ -52,8 +59,7 @@ impl MMR { pub fn verify_proof(&self, elem: &[u8], proof: &MMRProof) -> bool { let path_len = proof.path.len(); - let leaf = merkle::leaf(elem); - let root = merkle::path_root(leaf, &proof.path); + let root = proof.root(elem); for mmr_root in self.roots.iter() { if mmr_root.height == (path_len + 1) as u8 { diff --git a/emmarin/cl/cl/src/cl/mod.rs b/emmarin/cl/cl/src/cl/mod.rs index c2c25f6..31e7981 100644 --- a/emmarin/cl/cl/src/cl/mod.rs +++ b/emmarin/cl/cl/src/cl/mod.rs @@ -1,5 +1,4 @@ pub mod balance; -pub mod bundle; pub mod crypto; pub mod error; pub mod input; @@ -9,9 +8,9 @@ pub mod note; pub mod nullifier; pub mod output; pub mod partial_tx; +pub mod sparse_merkle; pub use balance::{Balance, BalanceWitness}; -pub use bundle::Bundle; pub use input::{Input, InputWitness}; pub use note::{Constraint, Nonce, NoteCommitment, NoteWitness}; pub use nullifier::{Nullifier, NullifierCommitment, NullifierSecret}; diff --git a/emmarin/cl/cl/src/cl/nullifier.rs b/emmarin/cl/cl/src/cl/nullifier.rs index c2340f4..9af8265 100644 --- a/emmarin/cl/cl/src/cl/nullifier.rs +++ b/emmarin/cl/cl/src/cl/nullifier.rs @@ -25,7 +25,7 @@ pub struct NullifierCommitment([u8; 32]); // The nullifier attached to input notes to prove an input has not // already been spent. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Nullifier([u8; 32]); +pub struct Nullifier(pub [u8; 32]); impl NullifierSecret { pub fn random(mut rng: impl RngCore) -> Self { diff --git a/emmarin/cl/cl/src/cl/sparse_merkle.rs b/emmarin/cl/cl/src/cl/sparse_merkle.rs new file mode 100644 index 0000000..bc1d035 --- /dev/null +++ b/emmarin/cl/cl/src/cl/sparse_merkle.rs @@ -0,0 +1,360 @@ +use std::collections::BTreeSet; + +use crate::cl::merkle; +use lazy_static::lazy_static; + +/// absence of element is marked with all 0's +pub static ABSENT: [u8; 32] = [0u8; 32]; + +/// presence of element is marked with all 1's +pub static PRESENT: [u8; 32] = [255u8; 32]; + +lazy_static! { + // the roots of empty merkle trees of diffent heights + // i.e. all leafs are ABSENT + static ref EMPTY_ROOTS: [[u8; 32]; 257] = { + let mut roots = [ABSENT; 257]; + for h in 1..257 { + roots[h] = merkle::node(roots[h - 1], roots[h - 1]); + } + + roots + }; +} + +pub fn sparse_root(elems: &BTreeSet<[u8; 32]>) -> [u8; 32] { + sparse_root_rec(0, elems) +} + +fn sparse_root_rec(prefix: u64, elems: &BTreeSet<[u8; 32]>) -> [u8; 32] { + if elems.is_empty() { + return empty_tree_root(256 - prefix); + } + if prefix == 256 { + assert_eq!(elems.len(), 1); + return PRESENT; + } + // partition the elements + let (left, right): (BTreeSet<_>, BTreeSet<_>) = + elems.iter().partition(|e| !bit(prefix as u8, **e)); + + merkle::node( + sparse_root_rec(prefix + 1, &left), + sparse_root_rec(prefix + 1, &right), + ) +} + +pub fn sparse_path(elem: [u8; 32], elems: &BTreeSet<[u8; 32]>) -> Vec { + fn sparse_path_rec( + prefix: u64, + elem: [u8; 32], + elems: &BTreeSet<[u8; 32]>, + ) -> Vec { + if prefix == 256 { + return Vec::new(); + } + // partition the elements + let (left, right): (BTreeSet<_>, BTreeSet<_>) = + elems.iter().partition(|e| !bit(prefix as u8, **e)); + + match bit(prefix as u8, elem) { + true => { + let left_root = sparse_root_rec(prefix + 1, &left); + let mut path = sparse_path_rec(prefix + 1, elem, &right); + + path.push(merkle::PathNode::Left(left_root)); + path + } + false => { + let right_root = sparse_root_rec(prefix + 1, &right); + let mut path = sparse_path_rec(prefix + 1, elem, &left); + + path.push(merkle::PathNode::Right(right_root)); + path + } + } + } + + sparse_path_rec(0, elem, elems) +} + +pub fn path_key(path: &[merkle::PathNode]) -> [u8; 32] { + assert_eq!(path.len(), 256); + + let mut key = [0u8; 32]; + for byte_i in (0..32).rev() { + let mut byte = 0u8; + for bit_i in 0..8 { + byte <<= 1; + match path[byte_i * 8 + bit_i] { + merkle::PathNode::Left(_) => byte += 1, + merkle::PathNode::Right(_) => byte += 0, + }; + } + key[31 - byte_i] = byte; + } + + key +} + +fn empty_tree_root(height: u64) -> [u8; 32] { + assert!(height <= 256); + EMPTY_ROOTS[height as usize] +} + +fn bit(idx: u8, elem: [u8; 32]) -> bool { + let byte = idx / 8; + let bit_in_byte = idx - byte * 8; + + (elem[byte as usize] & (1 << bit_in_byte)) != 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn random_hash() -> [u8; 32] { + rand::random() + } + + #[test] + fn test_neighbour_paths() { + let elems = BTreeSet::from_iter([[0u8; 32]]); + + let path_0 = sparse_path([0u8; 32], &elems); + let mut key_1 = [0u8; 32]; + key_1[31] = 128; + let path_1 = sparse_path(key_1, &elems); + + assert_ne!(path_0, path_1); + } + + #[test] + fn test_path_bit_agreement() { + fn path_bit(idx: u8, path: &[merkle::PathNode]) -> bool { + match path[255 - idx as usize] { + merkle::PathNode::Left(_) => true, + merkle::PathNode::Right(_) => false, + } + } + + let key = random_hash(); + let path = sparse_path(key, &BTreeSet::new()); + + for i in 0..=255 { + let b = bit(i, key); + let pb = path_bit(i, &path); + assert_eq!(b, pb, "{}!={}@{}", b, pb, i); + } + } + + #[test] + fn test_path_key() { + let elems = BTreeSet::from_iter(std::iter::repeat_with(random_hash).take(10)); + + // membership proofs + for e in elems.iter() { + let path = sparse_path(*e, &elems); + assert_eq!(path_key(&path), *e); + } + + // non-membership proofs + for _ in 0..10 { + let elem = random_hash(); + let path = sparse_path(elem, &elems); + assert_eq!(path_key(&path), elem); + } + } + + #[test] + fn test_sparse_path() { + let elems = BTreeSet::from_iter(std::iter::repeat_with(random_hash).take(10)); + + let root = sparse_root(&elems); + + // membership proofs + for e in elems.iter() { + let path = sparse_path(*e, &elems); + assert_eq!(merkle::path_root(PRESENT, &path), root); + } + + // non-membership proofs + for _ in 0..10 { + let elem = random_hash(); + let path = sparse_path(elem, &elems); + assert!(!elems.contains(&elem)); + assert_eq!(merkle::path_root(ABSENT, &path), root); + } + } + + #[test] + fn test_sparse_non_membership_in_empty_tree() { + let root = sparse_root(&BTreeSet::new()); + + let path = sparse_path([0u8; 32], &BTreeSet::new()); + + assert_eq!(merkle::path_root(ABSENT, &path), root); + + for (h, node) in path.into_iter().enumerate() { + match node { + merkle::PathNode::Left(hash) | merkle::PathNode::Right(hash) => { + assert_eq!(hash, empty_tree_root(h as u64)) + } + } + } + } + + #[test] + fn test_sparse_root_left_most_occupied() { + let root = sparse_root(&BTreeSet::from_iter([[0u8; 32]])); + + // We are constructing the tree: + // + // / \ + // / \ 0 subtree + // / \ 0 subtree + // 1 0 + let mut expected_root = PRESENT; + for h in 0..=255 { + expected_root = merkle::node(expected_root, empty_tree_root(h)) + } + + assert_eq!(root, expected_root) + } + + #[test] + fn test_sparse_root_right_most_occupied() { + let root = sparse_root(&BTreeSet::from_iter([[255u8; 32]])); + + // We are constructing the tree: + // + // /\ + // 0 /\ + // 0 /\ + // 0 1 + let mut expected_root = PRESENT; + for h in 0..=255 { + expected_root = merkle::node(empty_tree_root(h), expected_root) + } + + assert_eq!(root, expected_root) + } + + #[test] + fn test_sparse_root_middle_elem() { + let elem = { + let mut x = [255u8; 32]; + x[0] = 254; + x + }; + assert!(!bit(0, elem)); + for i in 1..=255 { + assert!(bit(i, elem)); + } + + let root = sparse_root(&BTreeSet::from_iter([elem])); + + // We are constructing the tree: + // root + // / \ + // /\ 0 + // 0 /\ + // 0 /\ + // 0 ... + // \ + // 1 + let mut expected_root = PRESENT; + for h in 0..=254 { + expected_root = merkle::node(empty_tree_root(h), expected_root) + } + expected_root = merkle::node(expected_root, empty_tree_root(255)); + + assert_eq!(root, expected_root) + } + + #[test] + fn test_sparse_root_middle_weave_elem() { + let elem = [85u8; 32]; + for i in 0..=255 { + assert_eq!(bit(i, elem), i % 2 == 0); + } + + let root = sparse_root(&BTreeSet::from_iter([elem])); + + // We are constructing the tree: + // /\ + // 0 /\ + // /\0 + // /\ + // 0 /\ + // /\0 + // 0 1 + + let mut expected_root = PRESENT; + for h in 0..=255 { + if h % 2 == 0 { + expected_root = merkle::node(expected_root, empty_tree_root(h)) + } else { + expected_root = merkle::node(empty_tree_root(h), expected_root) + } + } + assert_eq!(root, expected_root) + } + + #[test] + fn test_sparse_multiple_elems() { + let root = sparse_root(&BTreeSet::from_iter([[0u8; 32], [255u8; 32]])); + + // We are constructing the tree: + // root + // / \ + // /\ /\ + // /\0 0 /\ + // 1 0 0 1 + + let mut left_root = PRESENT; + for h in 0..=254 { + left_root = merkle::node(left_root, empty_tree_root(h)) + } + + let mut right_root = PRESENT; + for h in 0..=254 { + right_root = merkle::node(empty_tree_root(h), right_root) + } + let expected_root = merkle::node(left_root, right_root); + + assert_eq!(root, expected_root) + } + + #[test] + fn test_bit() { + for i in 0..=255 { + assert!(!bit(i, [0u8; 32])) + } + + for i in 0..=255 { + assert!(bit(i, [255u8; 32])) + } + + for i in 0..=255 { + assert_eq!(bit(i, [85u8; 32]), i % 2 == 0) + } + } + #[test] + fn test_empty_tree_root() { + assert_eq!(empty_tree_root(0), ABSENT); + + assert_eq!(empty_tree_root(1), merkle::node(ABSENT, ABSENT)); + assert_eq!( + empty_tree_root(2), + merkle::node(merkle::node(ABSENT, ABSENT), merkle::node(ABSENT, ABSENT)), + ); + assert_eq!( + empty_tree_root(3), + merkle::node( + merkle::node(merkle::node(ABSENT, ABSENT), merkle::node(ABSENT, ABSENT)), + merkle::node(merkle::node(ABSENT, ABSENT), merkle::node(ABSENT, ABSENT)), + ) + ); + } +} diff --git a/emmarin/cl/cl/src/zone_layer/ledger.rs b/emmarin/cl/cl/src/zone_layer/ledger.rs index 43d476b..6ac6c72 100644 --- a/emmarin/cl/cl/src/zone_layer/ledger.rs +++ b/emmarin/cl/cl/src/zone_layer/ledger.rs @@ -1,7 +1,11 @@ -use crate::cl::{merkle, mmr::MMR, Nullifier}; -use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; -const MAX_NULL: usize = 256; +use crate::cl::{ + merkle, + mmr::{MMRProof, MMR}, + sparse_merkle, NoteCommitment, Nullifier, +}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Ledger { @@ -12,23 +16,65 @@ pub struct Ledger { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LedgerWitness { pub commitments: MMR, - pub nullifiers: Vec, + pub nf_root: [u8; 32], } impl LedgerWitness { pub fn commit(&self) -> Ledger { Ledger { cm_root: self.commitments.commit(), + nf_root: self.nf_root, + } + } + + pub fn valid_cm_root(&self, root: [u8; 32]) -> bool { + self.commitments.roots.iter().any(|r| r.root == root) + } + + pub fn add_commitment(&mut self, cm: &NoteCommitment) { + self.commitments.push(&cm.0); + } + + pub fn assert_nf_update(&mut self, nf: &Nullifier, path: &[merkle::PathNode]) { + // verify that the path corresponds to the nullifier + assert_eq!(sparse_merkle::path_key(path), nf.0); + + // verify that the nullifier was not already present + assert_eq!(merkle::path_root(sparse_merkle::ABSENT, path), self.nf_root); + + // update the nullifer root with the nullifier inserted into the tree + self.nf_root = merkle::path_root(sparse_merkle::PRESENT, path); + } +} + +#[derive(Debug, Default, Clone)] +pub struct LedgerState { + pub commitments: MMR, + pub nullifiers: BTreeSet<[u8; 32]>, +} + +impl LedgerState { + pub fn to_witness(&self) -> LedgerWitness { + LedgerWitness { + commitments: self.commitments.clone(), nf_root: self.nf_root(), } } pub fn nf_root(&self) -> [u8; 32] { - let bytes = self - .nullifiers - .iter() - .map(|i| i.as_bytes().to_vec()) - .collect::>(); - merkle::root(merkle::padded_leaves::(&bytes)) + sparse_merkle::sparse_root(&self.nullifiers) + } + + pub fn add_commitment(&mut self, cm: &NoteCommitment) -> MMRProof { + self.commitments.push(&cm.0) + } + + pub fn add_nullifier(&mut self, nf: Nullifier) -> Vec { + let path = sparse_merkle::sparse_path(nf.0, &self.nullifiers); + + assert!(!self.nullifiers.contains(&nf.0)); + self.nullifiers.insert(nf.0); + + path } } diff --git a/emmarin/cl/ledger/Cargo.toml b/emmarin/cl/ledger/Cargo.toml index 053c813..6bde197 100644 --- a/emmarin/cl/ledger/Cargo.toml +++ b/emmarin/cl/ledger/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" cl = { path = "../cl" } ledger_proof_statements = { path = "../ledger_proof_statements" } nomos_cl_risc0_proofs = { path = "../risc0_proofs" } +nomos_cl_bundle_risc0_proof = { path = "../bundle_risc0_proof" } +nomos_cl_ptx_risc0_proof = { path = "../ptx_risc0_proof" } ledger_validity_proof = { path = "../ledger_validity_proof" } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-groth16 = { version = "1.0" } diff --git a/emmarin/cl/ledger/src/balance.rs b/emmarin/cl/ledger/src/balance.rs deleted file mode 100644 index d408782..0000000 --- a/emmarin/cl/ledger/src/balance.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::error::{Error, Result}; -use ledger_proof_statements::balance::{BalancePrivate, BalancePublic}; - -#[derive(Debug, Clone)] -pub struct ProvedBalance { - pub bundle: BalancePublic, - pub risc0_receipt: risc0_zkvm::Receipt, -} - -impl ProvedBalance { - pub fn prove(balance_witness: &BalancePrivate) -> Result { - //show that the sum of ptx balances is 0 - let env = risc0_zkvm::ExecutorEnv::builder() - .write(&balance_witness) - .unwrap() - .build() - .unwrap(); - - let prover = risc0_zkvm::default_prover(); - - let start_t = std::time::Instant::now(); - - let opts = risc0_zkvm::ProverOpts::succinct(); - let prove_info = prover - .prove_with_opts(env, nomos_cl_risc0_proofs::BALANCE_ELF, &opts) - .map_err(|_| Error::Risc0ProofFailed)?; - - println!( - "STARK 'bundle' prover time: {:.2?}, total_cycles: {}", - start_t.elapsed(), - prove_info.stats.total_cycles - ); - - let receipt = prove_info.receipt; - - Ok(Self { - bundle: receipt.journal.decode()?, - risc0_receipt: receipt, - }) - } - - pub fn public(&self) -> Result { - Ok(self.risc0_receipt.journal.decode()?) - } - - pub fn verify(&self) -> bool { - // let Ok(_bundle_public) = self.public() else { - // return false; - // }; - - // Vec::from_iter(self.bundle.partials.iter().map(|ptx| ptx.balance)) == bundle_public.balances - // && - self.risc0_receipt - .verify(nomos_cl_risc0_proofs::BALANCE_ID) - .is_ok() - } -} diff --git a/emmarin/cl/ledger/src/bundle.rs b/emmarin/cl/ledger/src/bundle.rs new file mode 100644 index 0000000..213f53b --- /dev/null +++ b/emmarin/cl/ledger/src/bundle.rs @@ -0,0 +1,52 @@ +use ledger_proof_statements::bundle::{BundlePrivate, BundlePublic}; + +use crate::partial_tx::ProvedPartialTx; + +#[derive(Debug, Clone)] +pub struct ProvedBundle { + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedBundle { + pub fn prove(bundle: &BundlePrivate, partials: Vec) -> Self { + //show that all ptx's are individually valid, and balance to 0 + let mut env = risc0_zkvm::ExecutorEnv::builder(); + + for proved_ptx in partials { + env.add_assumption(proved_ptx.risc0_receipt); + } + + let env = env.write(&bundle).unwrap().build().unwrap(); + + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, nomos_cl_bundle_risc0_proof::BUNDLE_ELF, &opts) + .unwrap(); + + println!( + "STARK 'bundle' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + let receipt = prove_info.receipt; + + Self { + risc0_receipt: receipt, + } + } + + pub fn public(&self) -> BundlePublic { + self.risc0_receipt.journal.decode().unwrap() + } + + pub fn verify(&self) -> bool { + self.risc0_receipt + .verify(nomos_cl_bundle_risc0_proof::BUNDLE_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/src/ledger.rs b/emmarin/cl/ledger/src/ledger.rs index 4875ba8..3b06621 100644 --- a/emmarin/cl/ledger/src/ledger.rs +++ b/emmarin/cl/ledger/src/ledger.rs @@ -1,64 +1,60 @@ -use ledger_proof_statements::{ - ledger::{LedgerProofPrivate, LedgerProofPublic}, - ptx::PtxPublic, -}; +use std::collections::BTreeMap; -use crate::{ - balance::ProvedBalance, - constraint::ConstraintProof, - error::{Error, Result}, - partial_tx::ProvedPartialTx, -}; -use cl::zone_layer::{ledger::LedgerWitness, notes::ZoneId}; +use ledger_proof_statements::ledger::{LedgerBundleWitness, LedgerProofPrivate, LedgerProofPublic}; + +use crate::bundle::ProvedBundle; +use cl::zone_layer::{ledger::LedgerState, notes::ZoneId}; #[derive(Debug, Clone)] pub struct ProvedLedgerTransition { - pub public: LedgerProofPublic, pub risc0_receipt: risc0_zkvm::Receipt, } -// TODO: find a better name -#[derive(Debug, Clone)] -pub struct ProvedBundle { - pub balance: ProvedBalance, - pub ptxs: Vec, -} - -impl ProvedBundle { - fn to_public(&self) -> Vec { - self.ptxs.iter().map(|p| p.public.clone()).collect() - } - - fn proofs(&self) -> Vec { - let mut proofs = vec![self.balance.risc0_receipt.clone()]; - proofs.extend(self.ptxs.iter().map(|p| p.risc0_receipt.clone())); - proofs - } -} - impl ProvedLedgerTransition { - pub fn prove( - ledger: LedgerWitness, - zone_id: ZoneId, - bundles: Vec, - constraints: Vec, - ) -> Result { - let witness = LedgerProofPrivate { - bundles: bundles.iter().map(|p| p.to_public()).collect(), - ledger, + pub fn prove(mut ledger: LedgerState, zone_id: ZoneId, bundles: Vec) -> Self { + let mut witness = LedgerProofPrivate { + bundles: Vec::new(), + ledger: ledger.to_witness(), id: zone_id, }; let mut env = risc0_zkvm::ExecutorEnv::builder(); - for bundle in bundles { - for proof in bundle.proofs() { - env.add_assumption(proof); + // prepare the sparse merkle tree nullifier proofs + for proved_bundle in &bundles { + env.add_assumption(proved_bundle.risc0_receipt.clone()); + + let bundle = proved_bundle.public(); + + let zone_ledger_update = bundle + .zone_ledger_updates + .get(&zone_id) + .expect("why are we proving this bundle for this zone if it's not involved?"); + + let cm_root_proofs = + BTreeMap::from_iter(zone_ledger_update.cm_roots.iter().map(|root| { + // We make the simplifying assumption that bundle proofs + // are done w.r.t. the latest MMR (hence, empty merkle proofs) + // + // We can remove this assumption by tracking old MMR roots in the LedgerState + (*root, vec![]) + })); + + let mut nf_proofs = Vec::new(); + for nf in &zone_ledger_update.nullifiers { + let nf_proof = ledger.add_nullifier(*nf); + nf_proofs.push(nf_proof); } + + let ledger_bundle = LedgerBundleWitness { + bundle, + cm_root_proofs, + nf_proofs, + }; + + witness.bundles.push(ledger_bundle) } - for covenant in constraints { - env.add_assumption(covenant.risc0_receipt); - } + let env = env.write(&witness).unwrap().build().unwrap(); // Obtain the default prover. @@ -71,10 +67,7 @@ impl ProvedLedgerTransition { let opts = risc0_zkvm::ProverOpts::succinct(); let prove_info = prover .prove_with_opts(env, ledger_validity_proof::LEDGER_ELF, &opts) - .map_err(|e| { - eprintln!("{e}"); - Error::Risc0ProofFailed - })?; + .unwrap(); println!( "STARK 'ledger' prover time: {:.2?}, total_cycles: {}", @@ -82,14 +75,16 @@ impl ProvedLedgerTransition { prove_info.stats.total_cycles ); - Ok(Self { - public: prove_info - .receipt - .journal - .decode::() - .unwrap(), + Self { risc0_receipt: prove_info.receipt, - }) + } + } + + pub fn public(&self) -> LedgerProofPublic { + self.risc0_receipt + .journal + .decode::() + .unwrap() } pub fn verify(&self) -> bool { diff --git a/emmarin/cl/ledger/src/lib.rs b/emmarin/cl/ledger/src/lib.rs index d42f46c..ad327b3 100644 --- a/emmarin/cl/ledger/src/lib.rs +++ b/emmarin/cl/ledger/src/lib.rs @@ -1,4 +1,4 @@ -pub mod balance; +pub mod bundle; pub mod constraint; pub mod error; pub mod ledger; diff --git a/emmarin/cl/ledger/src/partial_tx.rs b/emmarin/cl/ledger/src/partial_tx.rs index 0566f2a..4f73d95 100644 --- a/emmarin/cl/ledger/src/partial_tx.rs +++ b/emmarin/cl/ledger/src/partial_tx.rs @@ -1,31 +1,36 @@ use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; -use crate::error::{Error, Result}; -use cl::cl::{merkle, PartialTxWitness}; +use crate::{ + error::{Error, Result}, + ConstraintProof, +}; +use cl::cl::{ + mmr::{MMRProof, MMR}, + PartialTxWitness, +}; #[derive(Debug, Clone)] pub struct ProvedPartialTx { - pub public: PtxPublic, pub risc0_receipt: risc0_zkvm::Receipt, } impl ProvedPartialTx { pub fn prove( ptx_witness: PartialTxWitness, - input_cm_paths: Vec>, - cm_roots: Vec<[u8; 32]>, + input_cm_proofs: Vec<(MMR, MMRProof)>, + covenant_proofs: Vec, ) -> Result { let ptx_private = PtxPrivate { ptx: ptx_witness, - input_cm_paths, - cm_roots: cm_roots.clone(), + input_cm_proofs, }; - let env = risc0_zkvm::ExecutorEnv::builder() - .write(&ptx_private) - .unwrap() - .build() - .unwrap(); + let mut env = risc0_zkvm::ExecutorEnv::builder(); + + for covenant_proof in covenant_proofs { + env.add_assumption(covenant_proof.risc0_receipt); + } + let env = env.write(&ptx_private).unwrap().build().unwrap(); // Obtain the default prover. let prover = risc0_zkvm::default_prover(); @@ -36,7 +41,7 @@ impl ProvedPartialTx { // This struct contains the receipt along with statistics about execution of the guest let opts = risc0_zkvm::ProverOpts::succinct(); let prove_info = prover - .prove_with_opts(env, nomos_cl_risc0_proofs::PTX_ELF, &opts) + .prove_with_opts(env, nomos_cl_ptx_risc0_proof::PTX_ELF, &opts) .map_err(|_| Error::Risc0ProofFailed)?; println!( @@ -46,14 +51,17 @@ impl ProvedPartialTx { ); Ok(Self { - public: prove_info.receipt.journal.decode()?, risc0_receipt: prove_info.receipt, }) } + pub fn public(&self) -> PtxPublic { + self.risc0_receipt.journal.decode().unwrap() + } + pub fn verify(&self) -> bool { self.risc0_receipt - .verify(nomos_cl_risc0_proofs::PTX_ID) + .verify(nomos_cl_ptx_risc0_proof::PTX_ID) .is_ok() } } diff --git a/emmarin/cl/ledger/src/zone_update.rs b/emmarin/cl/ledger/src/zone_update.rs index 2bf7baa..1ffff33 100644 --- a/emmarin/cl/ledger/src/zone_update.rs +++ b/emmarin/cl/ledger/src/zone_update.rs @@ -18,16 +18,15 @@ impl ProvedUpdateBundle { return false; } - for bundle in &proof.public.cross_bundles { + for bundle in &proof.public().cross_bundles { expected_zones.insert(bundle.id, HashSet::from_iter(bundle.zones.clone())); actual_zones .entry(bundle.id) - .or_insert_with(|| HashSet::new()) - .insert(proof.public.id); + .or_insert_with(HashSet::new) + .insert(proof.public().id); } } - println!("{:?} | {:?}", expected_zones, actual_zones); for (bundle, expected) in expected_zones.iter() { if let Some(actual) = actual_zones.get(bundle) { if actual != expected { @@ -49,8 +48,8 @@ impl ProvedUpdateBundle { return false; } - if ledger_proof.public.old_ledger != update.old.ledger - || ledger_proof.public.ledger != update.new.ledger + if ledger_proof.public().old_ledger != update.old.ledger + || ledger_proof.public().ledger != update.new.ledger { return false; } diff --git a/emmarin/cl/ledger/tests/simple_transfer.rs b/emmarin/cl/ledger/tests/simple_transfer.rs index 03cf9af..b451b22 100644 --- a/emmarin/cl/ledger/tests/simple_transfer.rs +++ b/emmarin/cl/ledger/tests/simple_transfer.rs @@ -1,29 +1,28 @@ use cl::{ cl::{ - balance::Unit, merkle, mmr::MMR, note::derive_unit, BalanceWitness, InputWitness, - NoteWitness, NullifierCommitment, NullifierSecret, OutputWitness, PartialTxWitness, + balance::Unit, + mmr::{MMRProof, MMR}, + note::derive_unit, + BalanceWitness, InputWitness, NoteWitness, NullifierCommitment, NullifierSecret, + OutputWitness, PartialTxWitness, }, zone_layer::{ - ledger::LedgerWitness, + ledger::LedgerState, notes::{ZoneId, ZoneNote}, tx::{UpdateBundle, ZoneUpdate}, }, }; use ledger::{ - balance::ProvedBalance, - constraint::ConstraintProof, - ledger::{ProvedBundle, ProvedLedgerTransition}, - partial_tx::ProvedPartialTx, - stf::StfProof, - zone_update::ProvedUpdateBundle, + bundle::ProvedBundle, constraint::ConstraintProof, ledger::ProvedLedgerTransition, + partial_tx::ProvedPartialTx, stf::StfProof, zone_update::ProvedUpdateBundle, }; -use ledger_proof_statements::{balance::BalancePrivate, stf::StfPublic}; +use ledger_proof_statements::{bundle::BundlePrivate, stf::StfPublic}; use rand_core::CryptoRngCore; use std::sync::OnceLock; -fn nmo() -> &'static Unit { +fn nmo() -> Unit { static NMO: OnceLock = OnceLock::new(); - NMO.get_or_init(|| derive_unit("NMO")) + *NMO.get_or_init(|| derive_unit("NMO")) } struct User(NullifierSecret); @@ -48,24 +47,22 @@ fn receive_utxo(note: NoteWitness, nf_pk: NullifierCommitment, zone_id: ZoneId) fn cross_transfer_transition( input: InputWitness, - input_path: Vec, + input_proof: (MMR, MMRProof), to: User, amount: u64, zone_a: ZoneId, zone_b: ZoneId, - mut ledger_a: LedgerWitness, - mut ledger_b: LedgerWitness, + mut ledger_a: LedgerState, + mut ledger_b: LedgerState, ) -> (ProvedLedgerTransition, ProvedLedgerTransition) { - let mut rng = rand::thread_rng(); assert!(amount <= input.note.value); + + let mut rng = rand::thread_rng(); + let change = input.note.value - amount; - let transfer = OutputWitness::new( - NoteWitness::basic(amount, *nmo(), &mut rng), - to.pk(), - zone_b, - ); + let transfer = OutputWitness::new(NoteWitness::basic(amount, nmo(), &mut rng), to.pk(), zone_b); let change = OutputWitness::new( - NoteWitness::basic(change, *nmo(), &mut rng), + NoteWitness::basic(change, nmo(), &mut rng), input.nf_sk.commit(), zone_a, ); @@ -76,45 +73,46 @@ fn cross_transfer_transition( outputs: vec![transfer, change], balance_blinding: BalanceWitness::random_blinding(&mut rng), }; - let proved_ptx = ProvedPartialTx::prove( - ptx_witness.clone(), - vec![input_path], - vec![ledger_a.commitments.roots[0].root], - ) - .unwrap(); - - let balance = ProvedBalance::prove(&BalancePrivate { - balances: vec![ptx_witness.balance()], - }) - .unwrap(); - - let zone_tx = ProvedBundle { - ptxs: vec![proved_ptx.clone()], - balance, - }; // Prove the constraints for alices input (she uses the no-op constraint) let constraint_proof = - ConstraintProof::prove_nop(input.nullifier(), proved_ptx.public.ptx.root()); + ConstraintProof::prove_nop(input.nullifier(), ptx_witness.commit().root()); - let ledger_a_transition = ProvedLedgerTransition::prove( - ledger_a.clone(), - zone_a, - vec![zone_tx.clone()], - vec![constraint_proof], + let proved_ptx = ProvedPartialTx::prove( + ptx_witness.clone(), + vec![input_proof], + vec![constraint_proof.clone()], ) .unwrap(); - let ledger_b_transition = - ProvedLedgerTransition::prove(ledger_b.clone(), zone_b, vec![zone_tx], vec![]).unwrap(); + let bundle = ProvedBundle::prove( + &BundlePrivate { + bundle: vec![proved_ptx.public()], + balances: vec![ptx_witness.balance()], + }, + vec![proved_ptx], + ); - ledger_a.commitments.push(&change.commit_note().0); - ledger_a.nullifiers.push(input.nullifier()); + println!("proving ledger A transition"); + let ledger_a_transition = + ProvedLedgerTransition::prove(ledger_a.clone(), zone_a, vec![bundle.clone()]); - ledger_b.commitments.push(&transfer.commit_note().0); + println!("proving ledger B transition"); + let ledger_b_transition = ProvedLedgerTransition::prove(ledger_b.clone(), zone_b, vec![bundle]); - assert_eq!(ledger_a_transition.public.ledger, ledger_a.commit()); - assert_eq!(ledger_b_transition.public.ledger, ledger_b.commit()); + ledger_a.add_commitment(&change.commit_note()); + ledger_a.add_nullifier(input.nullifier()); + + ledger_b.add_commitment(&transfer.commit_note()); + + assert_eq!( + ledger_a_transition.public().ledger, + ledger_a.to_witness().commit() + ); + assert_eq!( + ledger_b_transition.public().ledger, + ledger_b.to_witness().commit() + ); (ledger_a_transition, ledger_b_transition) } @@ -133,42 +131,35 @@ fn zone_update_cross() { // Alice has an unspent note worth 10 NMO let utxo = receive_utxo( - NoteWitness::stateless(10, *nmo(), ConstraintProof::nop_constraint(), &mut rng), + NoteWitness::stateless(10, nmo(), ConstraintProof::nop_constraint(), &mut rng), alice.pk(), zone_a_id, ); let alice_input = InputWitness::from_output(utxo, alice.sk()); - let mut mmr = MMR::new(); - let input_cm_path = mmr.push(&utxo.commit_note().0).path; + let mut ledger_a = LedgerState::default(); + let alice_cm_path = ledger_a.add_commitment(&utxo.commit_note()); + let alice_cm_proof = (ledger_a.commitments.clone(), alice_cm_path); - let ledger_a = LedgerWitness { - commitments: mmr, - nullifiers: vec![], - }; - - let ledger_b = LedgerWitness { - commitments: MMR::new(), - nullifiers: vec![], - }; + let ledger_b = LedgerState::default(); let zone_a_old = ZoneNote { id: zone_a_id, state: [0; 32], - ledger: ledger_a.commit(), + ledger: ledger_a.to_witness().commit(), stf: [0; 32], }; let zone_b_old = ZoneNote { id: zone_b_id, state: [0; 32], - ledger: ledger_b.commit(), + ledger: ledger_b.to_witness().commit(), stf: [0; 32], }; let (ledger_a_transition, ledger_b_transition) = cross_transfer_transition( alice_input, - input_cm_path, + alice_cm_proof, bob, 8, zone_a_id, @@ -178,12 +169,12 @@ fn zone_update_cross() { ); let zone_a_new = ZoneNote { - ledger: ledger_a_transition.public.ledger, + ledger: ledger_a_transition.public().ledger, ..zone_a_old }; let zone_b_new = ZoneNote { - ledger: ledger_b_transition.public.ledger, + ledger: ledger_b_transition.public().ledger, ..zone_b_old }; diff --git a/emmarin/cl/ledger_proof_statements/Cargo.toml b/emmarin/cl/ledger_proof_statements/Cargo.toml index 65ea695..50bd1ca 100644 --- a/emmarin/cl/ledger_proof_statements/Cargo.toml +++ b/emmarin/cl/ledger_proof_statements/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] cl = { path = "../cl" } serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10" diff --git a/emmarin/cl/ledger_proof_statements/src/balance.rs b/emmarin/cl/ledger_proof_statements/src/balance.rs deleted file mode 100644 index 509b1b5..0000000 --- a/emmarin/cl/ledger_proof_statements/src/balance.rs +++ /dev/null @@ -1,12 +0,0 @@ -use cl::cl::{Balance, BalanceWitness}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BalancePublic { - pub balances: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BalancePrivate { - pub balances: Vec, -} diff --git a/emmarin/cl/ledger_proof_statements/src/bundle.rs b/emmarin/cl/ledger_proof_statements/src/bundle.rs new file mode 100644 index 0000000..abefbb3 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/bundle.rs @@ -0,0 +1,48 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use cl::{ + cl::{BalanceWitness, NoteCommitment, Nullifier}, + zone_layer::notes::ZoneId, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::ptx::PtxPublic; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct BundleId(pub [u8; 32]); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePublic { + pub bundle_id: BundleId, + pub zone_ledger_updates: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct LedgerUpdate { + // inputs in this bundle used the following roots in their cm membership proof. + pub cm_roots: BTreeSet<[u8; 32]>, + // these are the nullifiers of inputs used in this bundle. + pub nullifiers: Vec, + // these are commitments to created notes in this bundle + pub commitments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePrivate { + pub bundle: Vec, + pub balances: Vec, +} + +impl BundlePrivate { + pub fn id(&self) -> BundleId { + // TODO: change to merkle root + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_BUNDLE_ID"); + for ptx in &self.bundle { + hasher.update(ptx.ptx.root().0); + } + + BundleId(hasher.finalize().into()) + } +} diff --git a/emmarin/cl/ledger_proof_statements/src/ledger.rs b/emmarin/cl/ledger_proof_statements/src/ledger.rs index a7feda6..4766c89 100644 --- a/emmarin/cl/ledger_proof_statements/src/ledger.rs +++ b/emmarin/cl/ledger_proof_statements/src/ledger.rs @@ -1,5 +1,8 @@ -use crate::ptx::PtxPublic; -use cl::cl::{bundle::BundleId, Output}; +use std::collections::BTreeMap; + +use crate::bundle::BundleId; +use crate::bundle::BundlePublic; +use cl::cl::{merkle, NoteCommitment}; use cl::zone_layer::{ ledger::{Ledger, LedgerWitness}, notes::ZoneId, @@ -12,14 +15,21 @@ pub struct LedgerProofPublic { pub ledger: Ledger, pub id: ZoneId, pub cross_bundles: Vec, - pub outputs: Vec, + pub outputs: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LedgerProofPrivate { pub ledger: LedgerWitness, pub id: ZoneId, - pub bundles: Vec>, + pub bundles: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerBundleWitness { + pub bundle: BundlePublic, + pub cm_root_proofs: BTreeMap<[u8; 32], merkle::Path>, + pub nf_proofs: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/emmarin/cl/ledger_proof_statements/src/lib.rs b/emmarin/cl/ledger_proof_statements/src/lib.rs index 2000d4b..5d907dc 100644 --- a/emmarin/cl/ledger_proof_statements/src/lib.rs +++ b/emmarin/cl/ledger_proof_statements/src/lib.rs @@ -1,4 +1,4 @@ -pub mod balance; +pub mod bundle; pub mod constraint; pub mod ledger; pub mod ptx; diff --git a/emmarin/cl/ledger_proof_statements/src/ptx.rs b/emmarin/cl/ledger_proof_statements/src/ptx.rs index 93d555c..c058c35 100644 --- a/emmarin/cl/ledger_proof_statements/src/ptx.rs +++ b/emmarin/cl/ledger_proof_statements/src/ptx.rs @@ -1,15 +1,17 @@ -use cl::cl::{merkle, PartialTx, PartialTxWitness}; +use cl::cl::{ + mmr::{MMRProof, MMR}, + PartialTx, PartialTxWitness, +}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PtxPublic { pub ptx: PartialTx, - pub cm_roots: Vec<[u8; 32]>, + pub cm_mmrs: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PtxPrivate { pub ptx: PartialTxWitness, - pub input_cm_paths: Vec>, - pub cm_roots: Vec<[u8; 32]>, + pub input_cm_proofs: Vec<(MMR, MMRProof)>, } diff --git a/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml b/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml index a05b1fa..8b6ec52 100644 --- a/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml +++ b/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml @@ -10,7 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } ledger_proof_statements = { path = "../../ledger_proof_statements" } -nomos_cl_risc0_proofs = { path = "../../risc0_proofs" } +nomos_cl_bundle_risc0_proof = { path = "../../bundle_risc0_proof" } [patch.crates-io] # add RISC Zero accelerator support for all downstream usages of the following crates. diff --git a/emmarin/cl/ledger_validity_proof/ledger/src/main.rs b/emmarin/cl/ledger_validity_proof/ledger/src/main.rs index 5849b15..7e2dab8 100644 --- a/emmarin/cl/ledger_validity_proof/ledger/src/main.rs +++ b/emmarin/cl/ledger_validity_proof/ledger/src/main.rs @@ -1,12 +1,6 @@ -use cl::{ - cl::{Bundle, Output}, - zone_layer::{ledger::LedgerWitness, notes::ZoneId}, -}; +use cl::cl::merkle; use ledger_proof_statements::{ - balance::BalancePublic, - constraint::ConstraintPublic, - ledger::{CrossZoneBundle, LedgerProofPrivate, LedgerProofPublic}, - ptx::PtxPublic, + ledger::{CrossZoneBundle, LedgerProofPrivate, LedgerProofPublic, LedgerBundleWitness}, }; use risc0_zkvm::{guest::env, serde}; @@ -17,93 +11,42 @@ fn main() { bundles, } = env::read(); - let old_ledger = ledger.commit(); + let old_ledger = ledger.clone(); let mut cross_bundles = vec![]; let mut outputs = vec![]; - let roots = ledger - .commitments - .roots - .iter() - .map(|r| r.root) - .collect::>(); + for LedgerBundleWitness { bundle, cm_root_proofs, nf_proofs } in bundles { + env::verify(nomos_cl_bundle_risc0_proof::BUNDLE_ID, &serde::to_vec(&bundle).unwrap()).unwrap(); - for bundle in bundles { - let balance_public = BalancePublic { - balances: bundle.iter().map(|ptx| ptx.ptx.balance).collect::>(), - }; - // verify bundle is balanced - env::verify( - nomos_cl_risc0_proofs::BALANCE_ID, - &serde::to_vec(&balance_public).unwrap(), - ) - .unwrap(); + if let Some(ledger_update) = bundle.zone_ledger_updates.get(&id) { + for past_cm_root in &ledger_update.cm_roots { + let past_cm_root_proof = cm_root_proofs.get(past_cm_root).expect("missing cm root proof"); + let expected_current_cm_root = merkle::path_root(*past_cm_root, past_cm_root_proof); + assert!(old_ledger.valid_cm_root(expected_current_cm_root)) + } - for ptx in &bundle { - let (new_ledger, ptx_outputs) = process_ptx(ledger, ptx, id, &roots); - ledger = new_ledger; - outputs.extend(ptx_outputs); + assert_eq!(ledger_update.nullifiers.len(), nf_proofs.len()); + for (nf, nf_proof) in ledger_update.nullifiers.iter().zip(nf_proofs) { + ledger.assert_nf_update(nf, &nf_proof); + } + + for cm in &ledger_update.commitments { + ledger.add_commitment(cm); + outputs.push(*cm) + } } - let bundle = Bundle { - partials: bundle.into_iter().map(|ptx| ptx.ptx).collect(), - }; - let zones = bundle.zones(); - if zones.len() > 1 { - cross_bundles.push(CrossZoneBundle { - id: bundle.id(), - zones: zones.into_iter().collect(), - }); - } + cross_bundles.push(CrossZoneBundle { + id: bundle.bundle_id, + zones: bundle.zone_ledger_updates.into_keys().collect(), + }); } env::commit(&LedgerProofPublic { - old_ledger, + old_ledger: old_ledger.commit(), ledger: ledger.commit(), id, cross_bundles, outputs, }); } - -fn process_ptx( - mut ledger: LedgerWitness, - ptx: &PtxPublic, - zone_id: ZoneId, - roots: &[[u8; 32]], -) -> (LedgerWitness, Vec) { - // always verify the ptx to ensure outputs were derived with the correct zone id - env::verify(nomos_cl_risc0_proofs::PTX_ID, &serde::to_vec(&ptx).unwrap()).unwrap(); - - let cm_roots = &ptx.cm_roots; - let ptx = &ptx.ptx; - - let mut outputs = vec![]; - - for (input, input_cm_root) in ptx.inputs.iter().zip(cm_roots) { - if input.zone_id == zone_id { - assert!(roots.contains(input_cm_root)); - assert!(!ledger.nullifiers.contains(&input.nullifier)); - ledger.nullifiers.push(input.nullifier); - - env::verify( - input.constraint.0, - &serde::to_vec(&ConstraintPublic { - ptx_root: ptx.root(), - nf: input.nullifier, - }) - .unwrap(), - ) - .unwrap(); - } - } - - for output in &ptx.outputs { - if output.zone_id == zone_id { - ledger.commitments.push(&output.note_comm.0); - outputs.push(*output); - } - } - - (ledger, outputs) -} diff --git a/emmarin/cl/ptx_risc0_proof/Cargo.toml b/emmarin/cl/ptx_risc0_proof/Cargo.toml new file mode 100644 index 0000000..96515d9 --- /dev/null +++ b/emmarin/cl/ptx_risc0_proof/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nomos_cl_ptx_risc0_proof" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = ["ptx"] + diff --git a/emmarin/cl/ptx_risc0_proof/build.rs b/emmarin/cl/ptx_risc0_proof/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/emmarin/cl/ptx_risc0_proof/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/cl/risc0_proofs/ptx/Cargo.toml b/emmarin/cl/ptx_risc0_proof/ptx/Cargo.toml similarity index 100% rename from emmarin/cl/risc0_proofs/ptx/Cargo.toml rename to emmarin/cl/ptx_risc0_proof/ptx/Cargo.toml diff --git a/emmarin/cl/ptx_risc0_proof/ptx/src/main.rs b/emmarin/cl/ptx_risc0_proof/ptx/src/main.rs new file mode 100644 index 0000000..bbf862c --- /dev/null +++ b/emmarin/cl/ptx_risc0_proof/ptx/src/main.rs @@ -0,0 +1,43 @@ +/// Input Proof +use ledger_proof_statements::{ + constraint::ConstraintPublic, + ptx::{PtxPrivate, PtxPublic}, +}; +use risc0_zkvm::{guest::env, serde}; + +fn main() { + let PtxPrivate { + ptx, + input_cm_proofs, + } = env::read(); + + let ptx_commit = ptx.commit(); + let ptx_root = ptx_commit.root(); + + assert_eq!(ptx.inputs.len(), input_cm_proofs.len()); + let mut cm_mmrs = Vec::new(); + for (input, (mmr, mmr_proof)) in ptx.inputs.iter().zip(input_cm_proofs) { + let note_cm = input.note_commitment(); + assert!(mmr.verify_proof(¬e_cm.0, &mmr_proof)); + cm_mmrs.push(mmr); + + env::verify( + input.note.constraint.0, + &serde::to_vec(&ConstraintPublic { + ptx_root, + nf: input.nullifier(), + }) + .unwrap(), + ) + .unwrap(); + } + + for output in ptx.outputs.iter() { + assert!(output.note.value > 0); + } + + env::commit(&PtxPublic { + ptx: ptx_commit, + cm_mmrs, + }); +} diff --git a/emmarin/cl/ptx_risc0_proof/src/lib.rs b/emmarin/cl/ptx_risc0_proof/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/emmarin/cl/ptx_risc0_proof/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/emmarin/cl/risc0_proofs/Cargo.toml b/emmarin/cl/risc0_proofs/Cargo.toml index 19d562a..0c5b64d 100644 --- a/emmarin/cl/risc0_proofs/Cargo.toml +++ b/emmarin/cl/risc0_proofs/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" risc0-build = { version = "1.0" } [package.metadata.risc0] -methods = ["balance", "constraint_nop", "ptx", "stf_nop"] +methods = ["constraint_nop", "stf_nop"] diff --git a/emmarin/cl/risc0_proofs/balance/src/main.rs b/emmarin/cl/risc0_proofs/balance/src/main.rs deleted file mode 100644 index 3342001..0000000 --- a/emmarin/cl/risc0_proofs/balance/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -use cl::cl::BalanceWitness; -/// Bundle Proof -/// -/// The bundle proof demonstrates that the set of partial transactions -/// balance to zero. i.e. \sum inputs = \sum outputs. -/// -/// This is done by proving knowledge of some blinding factor `r` s.t. -/// \sum outputs - \sum input = 0*G + r*H -/// -/// To avoid doing costly ECC in stark, we compute only the RHS in stark. -/// The sums and equality is checked outside of stark during proof verification. -use risc0_zkvm::guest::env; - -fn main() { - let balance_private: ledger_proof_statements::balance::BalancePrivate = env::read(); - - let balance_public = ledger_proof_statements::balance::BalancePublic { - balances: Vec::from_iter(balance_private.balances.iter().map(|b| b.commit())), - }; - - assert!(BalanceWitness::combine(balance_private.balances, [0u8; 16]).is_zero()); - - env::commit(&balance_public); -} diff --git a/emmarin/cl/risc0_proofs/ptx/src/main.rs b/emmarin/cl/risc0_proofs/ptx/src/main.rs deleted file mode 100644 index 7b03f68..0000000 --- a/emmarin/cl/risc0_proofs/ptx/src/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -/// Input Proof -use cl::cl::merkle; -use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; -use risc0_zkvm::guest::env; - -fn main() { - let PtxPrivate { - ptx, - input_cm_paths, - cm_roots, - } = env::read(); - - assert_eq!(ptx.inputs.len(), input_cm_paths.len()); - for ((input, cm_path), cm_root) in ptx.inputs.iter().zip(input_cm_paths).zip(&cm_roots) { - let note_cm = input.note_commitment(); - let cm_leaf = merkle::leaf(note_cm.as_bytes()); - assert_eq!(*cm_root, merkle::path_root(cm_leaf, &cm_path)); - } - - for output in ptx.outputs.iter() { - assert!(output.note.value > 0); - } - - env::commit(&PtxPublic { - ptx: ptx.commit(), - cm_roots, - }); -}