diff --git a/emmarin/cl/.gitignore b/emmarin/cl/.gitignore new file mode 100644 index 0000000..b354aec --- /dev/null +++ b/emmarin/cl/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ \ No newline at end of file diff --git a/emmarin/cl/Cargo.toml b/emmarin/cl/Cargo.toml new file mode 100644 index 0000000..836a57c --- /dev/null +++ b/emmarin/cl/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = [ "cl", "ledger", "ledger_proof_statements", "risc0_proofs", "ledger_validity_proof"] + +# Always optimize; building and running the risc0_proofs takes much longer without optimization. +[profile.dev] +opt-level = 3 + +[profile.release] +debug = 1 +lto = true diff --git a/emmarin/cl/cl/Cargo.toml b/emmarin/cl/cl/Cargo.toml new file mode 100644 index 0000000..761fa4a --- /dev/null +++ b/emmarin/cl/cl/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = {version="1.0", features = ["derive"]} +group = "0.13.0" +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 diff --git a/emmarin/cl/cl/src/cl/balance.rs b/emmarin/cl/cl/src/cl/balance.rs new file mode 100644 index 0000000..1f2cc99 --- /dev/null +++ b/emmarin/cl/cl/src/cl/balance.rs @@ -0,0 +1,149 @@ +use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::cl::PartialTxWitness; + +pub type Value = u64; +pub type Unit = [u8; 32]; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub struct Balance([u8; 32]); + +impl Balance { + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct UnitBalance { + pub unit: Unit, + pub pos: u64, + pub neg: u64, +} + +impl UnitBalance { + pub fn is_zero(&self) -> bool { + self.pos == self.neg + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct BalanceWitness { + pub balances: Vec, + pub blinding: [u8; 16], +} + +impl BalanceWitness { + pub fn random_blinding(mut rng: impl CryptoRngCore) -> [u8; 16] { + let mut blinding = [0u8; 16]; + rng.fill_bytes(&mut blinding); + + blinding + } + + pub fn zero(blinding: [u8; 16]) -> Self { + Self { + balances: Default::default(), + blinding, + } + } + + pub fn from_ptx(ptx: &PartialTxWitness, blinding: [u8; 16]) -> Self { + let mut balance = Self::zero(blinding); + + for input in ptx.inputs.iter() { + balance.insert_negative(input.note.unit, input.note.value); + } + + for output in ptx.outputs.iter() { + balance.insert_positive(output.note.unit, output.note.value); + } + + balance.clear_zeros(); + + balance + } + + pub fn insert_positive(&mut self, unit: Unit, value: Value) { + for unit_bal in self.balances.iter_mut() { + if unit_bal.unit == unit { + unit_bal.pos += value; + return; + } + } + + // Unit was not found, so we must create one. + self.balances.push(UnitBalance { + unit, + pos: value, + neg: 0, + }); + } + + pub fn insert_negative(&mut self, unit: Unit, value: Value) { + for unit_bal in self.balances.iter_mut() { + if unit_bal.unit == unit { + unit_bal.neg += value; + return; + } + } + + self.balances.push(UnitBalance { + unit, + pos: 0, + neg: value, + }); + } + + pub fn clear_zeros(&mut self) { + let mut i = 0usize; + while i < self.balances.len() { + if self.balances[i].is_zero() { + self.balances.swap_remove(i); + // don't increment `i` since the last element has been swapped into the + // `i`'th place + } else { + i += 1; + } + } + } + + pub fn combine(balances: impl IntoIterator, blinding: [u8; 16]) -> Self { + let mut combined = BalanceWitness::zero(blinding); + + for balance in balances { + for unit_bal in balance.balances.iter() { + if unit_bal.pos > unit_bal.neg { + combined.insert_positive(unit_bal.unit, unit_bal.pos - unit_bal.neg); + } else { + combined.insert_negative(unit_bal.unit, unit_bal.neg - unit_bal.pos); + } + } + } + + combined.clear_zeros(); + + combined + } + + pub fn is_zero(&self) -> bool { + self.balances.is_empty() + } + + pub fn commit(&self) -> Balance { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_BAL_COMMIT"); + + for unit_balance in self.balances.iter() { + hasher.update(unit_balance.unit); + hasher.update(unit_balance.pos.to_le_bytes()); + hasher.update(unit_balance.neg.to_le_bytes()); + } + hasher.update(self.blinding); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + Balance(commit_bytes) + } +} diff --git a/emmarin/cl/cl/src/cl/bundle.rs b/emmarin/cl/cl/src/cl/bundle.rs new file mode 100644 index 0000000..aa32e08 --- /dev/null +++ b/emmarin/cl/cl/src/cl/bundle.rs @@ -0,0 +1,138 @@ +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/crypto.rs b/emmarin/cl/cl/src/cl/crypto.rs new file mode 100644 index 0000000..de35b1b --- /dev/null +++ b/emmarin/cl/cl/src/cl/crypto.rs @@ -0,0 +1,6 @@ +use curve25519_dalek::ristretto::RistrettoPoint; +use sha2::Sha512; + +pub fn hash_to_curve(bytes: &[u8]) -> RistrettoPoint { + RistrettoPoint::hash_from_bytes::(bytes) +} diff --git a/emmarin/cl/cl/src/cl/error.rs b/emmarin/cl/cl/src/cl/error.rs new file mode 100644 index 0000000..03d01d1 --- /dev/null +++ b/emmarin/cl/cl/src/cl/error.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum Error { + ProofFailed, +} diff --git a/emmarin/cl/cl/src/cl/input.rs b/emmarin/cl/cl/src/cl/input.rs new file mode 100644 index 0000000..56a6a08 --- /dev/null +++ b/emmarin/cl/cl/src/cl/input.rs @@ -0,0 +1,97 @@ +/// This module defines the partial transaction structure. +/// +/// Partial transactions, as the name suggests, are transactions +/// which on their own may not balance (i.e. \sum inputs != \sum outputs) +use crate::{ + cl::{ + note::{Constraint, NoteWitness}, + nullifier::{Nullifier, NullifierSecret}, + Nonce, NoteCommitment, OutputWitness, + }, + zone_layer::notes::ZoneId, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Input { + pub nullifier: Nullifier, + pub constraint: Constraint, + pub zone_id: ZoneId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct InputWitness { + pub note: NoteWitness, + pub nf_sk: NullifierSecret, + pub zone_id: ZoneId, +} + +impl InputWitness { + pub fn new(note: NoteWitness, nf_sk: NullifierSecret, zone_id: ZoneId) -> Self { + Self { + note, + nf_sk, + zone_id, + } + } + + pub fn from_output(output: OutputWitness, nf_sk: NullifierSecret) -> Self { + assert_eq!(nf_sk.commit(), output.nf_pk); + Self::new(output.note, nf_sk, output.zone_id) + } + + pub fn public(output: OutputWitness) -> Self { + let nf_sk = NullifierSecret::zero(); + assert_eq!(nf_sk.commit(), output.nf_pk); // ensure the output was a public UTXO + Self::new(output.note, nf_sk, output.zone_id) + } + + pub fn evolved_nonce(&self, domain: &[u8]) -> Nonce { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_COIN_EVOLVE"); + hasher.update(domain); + hasher.update(self.nf_sk.0); + hasher.update(self.note.commit(&self.zone_id, self.nf_sk.commit()).0); + + let nonce_bytes: [u8; 32] = hasher.finalize().into(); + Nonce::from_bytes(nonce_bytes) + } + + pub fn evolve_output(&self, domain: &[u8]) -> OutputWitness { + OutputWitness { + note: NoteWitness { + nonce: self.evolved_nonce(domain), + ..self.note + }, + nf_pk: self.nf_sk.commit(), + zone_id: self.zone_id, + } + } + + pub fn nullifier(&self) -> Nullifier { + Nullifier::new(&self.zone_id, self.nf_sk, self.note_commitment()) + } + + pub fn commit(&self) -> Input { + Input { + nullifier: self.nullifier(), + constraint: self.note.constraint, + zone_id: self.zone_id, + } + } + + pub fn note_commitment(&self) -> NoteCommitment { + self.note.commit(&self.zone_id, self.nf_sk.commit()) + } +} + +impl Input { + pub fn to_bytes(&self) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..32].copy_from_slice(self.nullifier.as_bytes()); + bytes[32..64].copy_from_slice(&self.constraint.0); + bytes[64..96].copy_from_slice(&self.zone_id); + bytes + } +} diff --git a/emmarin/cl/cl/src/cl/merkle.rs b/emmarin/cl/cl/src/cl/merkle.rs new file mode 100644 index 0000000..bb10e3c --- /dev/null +++ b/emmarin/cl/cl/src/cl/merkle.rs @@ -0,0 +1,239 @@ +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +pub fn padded_leaves(elements: &[Vec]) -> [[u8; 32]; N] { + let mut leaves = [[0u8; 32]; N]; + + for (i, element) in elements.iter().enumerate() { + assert!(i < N); + leaves[i] = leaf(element); + } + + leaves +} + +pub fn leaf(data: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_MERKLE_LEAF"); + hasher.update(data); + hasher.finalize().into() +} + +pub fn node(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_MERKLE_NODE"); + hasher.update(a); + hasher.update(b); + hasher.finalize().into() +} + +pub fn root(elements: [[u8; 32]; N]) -> [u8; 32] { + let n = elements.len(); + + assert!(n.is_power_of_two()); + + let mut nodes = elements; + + for h in (1..=n.ilog2()).rev() { + for i in 0..2usize.pow(h - 1) { + nodes[i] = node(nodes[i * 2], nodes[i * 2 + 1]); + } + } + + nodes[0] +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PathNode { + Left([u8; 32]), + Right([u8; 32]), +} + +pub fn path_root(leaf: [u8; 32], path: &[PathNode]) -> [u8; 32] { + let mut computed_hash = leaf; + + for path_node in path { + match path_node { + PathNode::Left(sibling_hash) => { + computed_hash = node(*sibling_hash, computed_hash); + } + PathNode::Right(sibling_hash) => { + computed_hash = node(computed_hash, *sibling_hash); + } + } + } + + computed_hash +} + +pub fn path(leaves: [[u8; 32]; N], idx: usize) -> Vec { + assert!(N.is_power_of_two()); + assert!(idx < N); + + let mut nodes = leaves; + let mut path = Vec::new(); + let mut idx = idx; + + for h in (1..=N.ilog2()).rev() { + if idx % 2 == 0 { + path.push(PathNode::Right(nodes[idx + 1])); + } else { + path.push(PathNode::Left(nodes[idx - 1])); + } + + idx /= 2; + + for i in 0..2usize.pow(h - 1) { + nodes[i] = node(nodes[i * 2], nodes[i * 2 + 1]); + } + } + + path +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_root_height_1() { + let r = root::<1>(padded_leaves(&[b"sand".into()])); + + let expected = leaf(b"sand"); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_2() { + let r = root::<2>(padded_leaves(&[b"desert".into(), b"sand".into()])); + + let expected = node(leaf(b"desert"), leaf(b"sand")); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_3() { + let r = root::<4>(padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + ])); + + let expected = node( + node(leaf(b"desert"), leaf(b"sand")), + node(leaf(b"feels"), leaf(b"warm")), + ); + + assert_eq!(r, expected); + } + + #[test] + fn test_root_height_4() { + let r = root::<8>(padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + b"at".into(), + b"night".into(), + ])); + + let expected = node( + node( + node(leaf(b"desert"), leaf(b"sand")), + node(leaf(b"feels"), leaf(b"warm")), + ), + node( + node(leaf(b"at"), leaf(b"night")), + node([0u8; 32], [0u8; 32]), + ), + ); + + assert_eq!(r, expected); + } + + #[test] + fn test_path_height_1() { + let leaves = padded_leaves(&[b"desert".into()]); + let r = root::<1>(leaves); + + let p = path::<1>(leaves, 0); + let expected = vec![]; + assert_eq!(p, expected); + assert_eq!(path_root(leaf(b"desert"), &p), r); + } + + #[test] + fn test_path_height_2() { + let leaves = padded_leaves(&[b"desert".into(), b"sand".into()]); + let r = root::<2>(leaves); + + // --- proof for element at idx 0 + + let p0 = path(leaves, 0); + let expected0 = vec![PathNode::Right(leaf(b"sand"))]; + assert_eq!(p0, expected0); + assert_eq!(path_root(leaf(b"desert"), &p0), r); + + // --- proof for element at idx 1 + + let p1 = path(leaves, 1); + let expected1 = vec![PathNode::Left(leaf(b"desert"))]; + assert_eq!(p1, expected1); + assert_eq!(path_root(leaf(b"sand"), &p1), r); + } + + #[test] + fn test_path_height_3() { + let leaves = padded_leaves(&[ + b"desert".into(), + b"sand".into(), + b"feels".into(), + b"warm".into(), + ]); + let r = root::<4>(leaves); + + // --- proof for element at idx 0 + + let p0 = path(leaves, 0); + let expected0 = vec![ + PathNode::Right(leaf(b"sand")), + PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))), + ]; + assert_eq!(p0, expected0); + assert_eq!(path_root(leaf(b"desert"), &p0), r); + + // --- proof for element at idx 1 + + let p1 = path(leaves, 1); + let expected1 = vec![ + PathNode::Left(leaf(b"desert")), + PathNode::Right(node(leaf(b"feels"), leaf(b"warm"))), + ]; + assert_eq!(p1, expected1); + assert_eq!(path_root(leaf(b"sand"), &p1), r); + + // --- proof for element at idx 2 + + let p2 = path(leaves, 2); + let expected2 = vec![ + PathNode::Right(leaf(b"warm")), + PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))), + ]; + assert_eq!(p2, expected2); + assert_eq!(path_root(leaf(b"feels"), &p2), r); + + // --- proof for element at idx 3 + + let p3 = path(leaves, 3); + let expected3 = vec![ + PathNode::Left(leaf(b"feels")), + PathNode::Left(node(leaf(b"desert"), leaf(b"sand"))), + ]; + assert_eq!(p3, expected3); + assert_eq!(path_root(leaf(b"warm"), &p3), r); + } +} diff --git a/emmarin/cl/cl/src/cl/mmr.rs b/emmarin/cl/cl/src/cl/mmr.rs new file mode 100644 index 0000000..a5a4151 --- /dev/null +++ b/emmarin/cl/cl/src/cl/mmr.rs @@ -0,0 +1,127 @@ +use crate::cl::merkle; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MMR { + pub roots: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Root { + pub root: [u8; 32], + pub height: u8, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MMRProof { + pub path: Vec, +} + +impl MMR { + pub fn new() -> Self { + Self { roots: vec![] } + } + + pub fn push(&mut self, elem: &[u8]) -> MMRProof { + let new_root = Root { + root: merkle::leaf(elem), + height: 1, + }; + self.roots.push(new_root); + + let mut path = vec![]; + + for i in (1..self.roots.len()).rev() { + if self.roots[i].height == self.roots[i - 1].height { + path.push(merkle::PathNode::Left(self.roots[i - 1].root)); + + self.roots[i - 1] = Root { + root: merkle::node(self.roots[i - 1].root, self.roots[i].root), + height: self.roots[i - 1].height + 1, + }; + + self.roots.remove(i); + } else { + break; + } + } + + MMRProof { path } + } + + 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); + + for mmr_root in self.roots.iter() { + if mmr_root.height == (path_len + 1) as u8 { + return mmr_root.root == root; + } + } + + false + } + + pub fn commit(&self) -> [u8; 32] { + // todo: baggin the peaks + let mut hasher = Sha256::new(); + for mmr_root in self.roots.iter() { + hasher.update(mmr_root.root); + hasher.update(mmr_root.height.to_le_bytes()); + } + hasher.finalize().into() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_mmr_push() { + let mut mmr = MMR::new(); + let proof = mmr.push(b"hello"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 1); + assert_eq!(mmr.roots[0].root, merkle::leaf(b"hello")); + assert!(mmr.verify_proof(b"hello", &proof)); + + let proof = mmr.push(b"world"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 2); + assert_eq!( + mmr.roots[0].root, + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")) + ); + assert!(mmr.verify_proof(b"world", &proof)); + + let proof = mmr.push(b"!"); + + assert_eq!(mmr.roots.len(), 2); + assert_eq!(mmr.roots[0].height, 2); + assert_eq!( + mmr.roots[0].root, + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")) + ); + assert_eq!(mmr.roots[1].height, 1); + assert_eq!(mmr.roots[1].root, merkle::leaf(b"!")); + assert!(mmr.verify_proof(b"!", &proof)); + + let proof = mmr.push(b"!"); + + assert_eq!(mmr.roots.len(), 1); + assert_eq!(mmr.roots[0].height, 3); + assert_eq!( + mmr.roots[0].root, + merkle::node( + merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")), + merkle::node(merkle::leaf(b"!"), merkle::leaf(b"!")) + ) + ); + assert!(mmr.verify_proof(b"!", &proof)); + } +} diff --git a/emmarin/cl/cl/src/cl/mod.rs b/emmarin/cl/cl/src/cl/mod.rs new file mode 100644 index 0000000..c2c25f6 --- /dev/null +++ b/emmarin/cl/cl/src/cl/mod.rs @@ -0,0 +1,21 @@ +pub mod balance; +pub mod bundle; +pub mod crypto; +pub mod error; +pub mod input; +pub mod merkle; +pub mod mmr; +pub mod note; +pub mod nullifier; +pub mod output; +pub mod partial_tx; + +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}; +pub use output::{Output, OutputWitness}; +pub use partial_tx::{ + PartialTx, PartialTxInputWitness, PartialTxOutputWitness, PartialTxWitness, PtxRoot, +}; diff --git a/emmarin/cl/cl/src/cl/note.rs b/emmarin/cl/cl/src/cl/note.rs new file mode 100644 index 0000000..e9b6c31 --- /dev/null +++ b/emmarin/cl/cl/src/cl/note.rs @@ -0,0 +1,172 @@ +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::cl::{balance::Unit, nullifier::NullifierCommitment}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Constraint(pub [u8; 32]); + +impl Constraint { + pub fn from_vk(constraint_vk: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_CONSTRAINT_COMMIT"); + hasher.update(constraint_vk); + let constraint_cm: [u8; 32] = hasher.finalize().into(); + + Self(constraint_cm) + } +} + +pub fn derive_unit(unit: &str) -> Unit { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_UNIT"); + hasher.update(unit.as_bytes()); + let unit: Unit = hasher.finalize().into(); + unit +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct NoteCommitment(pub [u8; 32]); + +impl NoteCommitment { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub struct NoteWitness { + pub value: u64, + pub unit: Unit, + pub constraint: Constraint, + pub state: [u8; 32], + pub nonce: Nonce, +} + +impl NoteWitness { + pub fn new( + value: u64, + unit: Unit, + constraint: Constraint, + state: [u8; 32], + nonce: Nonce, + ) -> Self { + Self { + value, + unit, + constraint, + state, + nonce, + } + } + + pub fn basic(value: u64, unit: Unit, rng: impl RngCore) -> Self { + let constraint = Constraint([0u8; 32]); + let nonce = Nonce::random(rng); + Self::new(value, unit, constraint, [0u8; 32], nonce) + } + + pub fn stateless(value: u64, unit: Unit, constraint: Constraint, rng: impl RngCore) -> Self { + Self::new(value, unit, constraint, [0u8; 32], Nonce::random(rng)) + } + + pub fn commit(&self, tag: &dyn AsRef<[u8]>, nf_pk: NullifierCommitment) -> NoteCommitment { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_NOTE_CM"); + hasher.update(tag.as_ref()); + + // COMMIT TO BALANCE + hasher.update(self.value.to_le_bytes()); + hasher.update(self.unit); + // Important! we don't commit to the balance blinding factor as that may make the notes linkable. + + // COMMIT TO STATE + hasher.update(self.state); + + // COMMIT TO CONSTRAINT + hasher.update(self.constraint.0); + + // COMMIT TO NONCE + hasher.update(self.nonce.as_bytes()); + + // COMMIT TO NULLIFIER + hasher.update(nf_pk.as_bytes()); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + NoteCommitment(commit_bytes) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Nonce([u8; 32]); + +impl Nonce { + pub fn random(mut rng: impl RngCore) -> Self { + let mut nonce = [0u8; 32]; + rng.fill_bytes(&mut nonce); + Self(nonce) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +#[cfg(test)] +mod test { + // use super::*; + // use crate::cl::nullifier::NullifierSecret; + + // #[test] + // fn test_note_commit_permutations() { + // let (nmo, eth) = (derive_unit("NMO"), derive_unit("ETH")); + + // let mut rng = rand::thread_rng(); + + // let nf_pk = NullifierSecret::random(&mut rng).commit(); + + // let reference_note = NoteWitness::basic(32, nmo, &mut rng); + + // // different notes under same nullifier produce different commitments + // let mutation_tests = [ + // NoteWitness { + // value: 12, + // ..reference_note + // }, + // NoteWitness { + // unit: eth, + // ..reference_note + // }, + // NoteWitness { + // constraint: Constraint::from_vk(&[1u8; 32]), + // ..reference_note + // }, + // NoteWitness { + // state: [1u8; 32], + // ..reference_note + // }, + // NoteWitness { + // nonce: Nonce::random(&mut rng), + // ..reference_note + // }, + // ]; + + // for n in mutation_tests { + // assert_ne!(n.commit(nf_pk), reference_note.commit(nf_pk)); + // } + + // // commitment to same note with different nullifiers produce different commitments + + // let other_nf_pk = NullifierSecret::random(&mut rng).commit(); + + // assert_ne!( + // reference_note.commit(nf_pk), + // reference_note.commit(other_nf_pk) + // ); + // } +} diff --git a/emmarin/cl/cl/src/cl/nullifier.rs b/emmarin/cl/cl/src/cl/nullifier.rs new file mode 100644 index 0000000..c2340f4 --- /dev/null +++ b/emmarin/cl/cl/src/cl/nullifier.rs @@ -0,0 +1,161 @@ +// The Nullifier is used to detect if a note has +// already been consumed. + +// The same nullifier secret may be used across multiple +// notes to allow users to hold fewer secrets. A note +// nonce is used to disambiguate when the same nullifier +// secret is used for multiple notes. + +use rand_core::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::cl::NoteCommitment; + +// Maintained privately by note holder +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct NullifierSecret(pub [u8; 16]); + +// Nullifier commitment is public information that +// can be provided to anyone wishing to transfer +// you a note +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +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]); + +impl NullifierSecret { + pub fn random(mut rng: impl RngCore) -> Self { + let mut sk = [0u8; 16]; + rng.fill_bytes(&mut sk); + Self(sk) + } + + pub const fn zero() -> Self { + Self([0u8; 16]) + } + + pub fn commit(&self) -> NullifierCommitment { + let mut hasher = Sha256::new(); + hasher.update(b"NOMOS_CL_NULL_COMMIT"); + hasher.update(self.0); + + let commit_bytes: [u8; 32] = hasher.finalize().into(); + NullifierCommitment(commit_bytes) + } + + pub fn from_bytes(bytes: [u8; 16]) -> Self { + Self(bytes) + } +} + +impl NullifierCommitment { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn hex(&self) -> String { + hex::encode(self.0) + } + + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl Nullifier { + pub fn new(tag: &dyn AsRef<[u8]>, sk: NullifierSecret, note_cm: NoteCommitment) -> Self { + let mut hasher = Sha256::new(); + hasher.update(tag.as_ref()); + hasher.update(sk.0); + hasher.update(note_cm.0); + + let nf_bytes: [u8; 32] = hasher.finalize().into(); + Self(nf_bytes) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[cfg(test)] +mod test { + // use crate::cl::{note::derive_unit, Constraint, Nonce, NoteWitness}; + + // use super::*; + + // #[ignore = "nullifier test vectors not stable yet"] + // #[test] + // fn test_nullifier_commitment_vectors() { + // assert_eq!( + // NullifierSecret([0u8; 16]).commit().hex(), + // "384318f9864fe57647bac344e2afdc500a672dedb29d2dc63b004e940e4b382a" + // ); + // assert_eq!( + // NullifierSecret([1u8; 16]).commit().hex(), + // "0fd667e6bb39fbdc35d6265726154b839638ea90bcf4e736953ccf27ca5f870b" + // ); + // assert_eq!( + // NullifierSecret([u8::MAX; 16]).commit().hex(), + // "1cb78e487eb0b3116389311fdde84cd3f619a4d7f487b29bf5a002eed3784d75" + // ); + // } + + // #[test] + // fn test_nullifier_same_sk_different_nonce() { + // let mut rng = rand::thread_rng(); + // let sk = NullifierSecret::random(&mut rng); + // let note_1 = NoteWitness { + // value: 1, + // unit: derive_unit("NMO"), + // constraint: Constraint::from_vk(&[]), + // state: [0u8; 32], + // nonce: Nonce::random(&mut rng), + // }; + // let note_2 = NoteWitness { + // nonce: Nonce::random(&mut rng), + // ..note_1 + // }; + + // let note_cm_1 = note_1.commit(sk.commit()); + // let note_cm_2 = note_2.commit(sk.commit()); + + // let nf_1 = Nullifier::new(sk, note_cm_1); + // let nf_2 = Nullifier::new(sk, note_cm_2); + + // assert_ne!(nf_1, nf_2); + // } + + // #[test] + // fn test_same_sk_same_nonce_different_note() { + // let mut rng = rand::thread_rng(); + + // let sk = NullifierSecret::random(&mut rng); + // let nonce = Nonce::random(&mut rng); + + // let note_1 = NoteWitness { + // value: 1, + // unit: derive_unit("NMO"), + // constraint: Constraint::from_vk(&[]), + // state: [0u8; 32], + // nonce, + // }; + + // let note_2 = NoteWitness { + // unit: derive_unit("ETH"), + // ..note_1 + // }; + + // let note_cm_1 = note_1.commit(sk.commit()); + // let note_cm_2 = note_2.commit(sk.commit()); + + // let nf_1 = Nullifier::new(sk, note_cm_1); + // let nf_2 = Nullifier::new(sk, note_cm_2); + + // assert_ne!(nf_1, nf_2); + // } +} diff --git a/emmarin/cl/cl/src/cl/output.rs b/emmarin/cl/cl/src/cl/output.rs new file mode 100644 index 0000000..0bcc377 --- /dev/null +++ b/emmarin/cl/cl/src/cl/output.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + cl::{ + note::{NoteCommitment, NoteWitness}, + nullifier::NullifierCommitment, + NullifierSecret, + }, + zone_layer::notes::ZoneId, +}; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct Output { + pub zone_id: ZoneId, + pub note_comm: NoteCommitment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputWitness { + pub note: NoteWitness, + pub nf_pk: NullifierCommitment, + pub zone_id: ZoneId, +} + +impl OutputWitness { + pub fn new(note: NoteWitness, nf_pk: NullifierCommitment, zone_id: ZoneId) -> Self { + Self { + note, + nf_pk, + zone_id, + } + } + + pub fn public(note: NoteWitness, zone_id: ZoneId) -> Self { + let nf_pk = NullifierSecret::zero().commit(); + Self { + note, + nf_pk, + zone_id, + } + } + + pub fn commit_note(&self) -> NoteCommitment { + self.note.commit(&self.zone_id, self.nf_pk) + } + + pub fn commit(&self) -> Output { + Output { + zone_id: self.zone_id, + note_comm: self.commit_note(), + } + } +} + +impl Output { + pub fn to_bytes(&self) -> [u8; 64] { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(&self.zone_id); + bytes[32..].copy_from_slice(&self.note_comm.0); + bytes + } +} diff --git a/emmarin/cl/cl/src/cl/partial_tx.rs b/emmarin/cl/cl/src/cl/partial_tx.rs new file mode 100644 index 0000000..f4db0e4 --- /dev/null +++ b/emmarin/cl/cl/src/cl/partial_tx.rs @@ -0,0 +1,213 @@ +use rand_core::{CryptoRngCore, RngCore}; +use serde::{Deserialize, Serialize}; + +use crate::cl::{ + balance::{Balance, BalanceWitness}, + input::{Input, InputWitness}, + merkle, + output::{Output, OutputWitness}, +}; + +pub const MAX_INPUTS: usize = 8; +pub const MAX_OUTPUTS: usize = 8; + +/// The partial transaction commitment couples an input to a partial transaction. +/// Prevents partial tx unbundling. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct PtxRoot(pub [u8; 32]); + +impl From<[u8; 32]> for PtxRoot { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl PtxRoot { + pub fn random(mut rng: impl RngCore) -> Self { + let mut sk = [0u8; 32]; + rng.fill_bytes(&mut sk); + Self(sk) + } + + pub fn hex(&self) -> String { + hex::encode(self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTx { + pub inputs: Vec, + pub outputs: Vec, + pub balance: Balance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxWitness { + pub inputs: Vec, + pub outputs: Vec, + pub balance_blinding: [u8; 16], +} + +impl PartialTxWitness { + pub fn random( + inputs: Vec, + outputs: Vec, + mut rng: impl CryptoRngCore, + ) -> Self { + Self { + inputs, + outputs, + balance_blinding: BalanceWitness::random_blinding(&mut rng), + } + } + + pub fn balance(&self) -> BalanceWitness { + BalanceWitness::from_ptx(self, self.balance_blinding) + } + + pub fn commit(&self) -> PartialTx { + PartialTx { + inputs: self.inputs.iter().map(InputWitness::commit).collect(), + outputs: self.outputs.iter().map(OutputWitness::commit).collect(), + balance: self.balance().commit(), + } + } + + pub fn input_witness(&self, idx: usize) -> PartialTxInputWitness { + let input_bytes = + Vec::from_iter(self.inputs.iter().map(|i| i.commit().to_bytes().to_vec())); + let input_merkle_leaves = merkle::padded_leaves::(&input_bytes); + + let path = merkle::path(input_merkle_leaves, idx); + let input = self.inputs[idx]; + PartialTxInputWitness { input, path } + } + + pub fn output_witness(&self, idx: usize) -> PartialTxOutputWitness { + let output_bytes = + Vec::from_iter(self.outputs.iter().map(|o| o.commit().to_bytes().to_vec())); + let output_merkle_leaves = merkle::padded_leaves::(&output_bytes); + + let path = merkle::path(output_merkle_leaves, idx); + let output = self.outputs[idx]; + PartialTxOutputWitness { output, path } + } +} + +impl PartialTx { + pub fn input_root(&self) -> [u8; 32] { + let input_bytes = + Vec::from_iter(self.inputs.iter().map(Input::to_bytes).map(Vec::from_iter)); + let input_merkle_leaves = merkle::padded_leaves(&input_bytes); + merkle::root::(input_merkle_leaves) + } + + pub fn output_root(&self) -> [u8; 32] { + let output_bytes = Vec::from_iter( + self.outputs + .iter() + .map(Output::to_bytes) + .map(Vec::from_iter), + ); + let output_merkle_leaves = merkle::padded_leaves(&output_bytes); + merkle::root::(output_merkle_leaves) + } + + pub fn root(&self) -> PtxRoot { + let input_root = self.input_root(); + let output_root = self.output_root(); + let root = merkle::node(input_root, output_root); + PtxRoot(root) + } +} + +/// An input to a partial transaction +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxInputWitness { + pub input: InputWitness, + pub path: Vec, +} + +impl PartialTxInputWitness { + pub fn input_root(&self) -> [u8; 32] { + let leaf = merkle::leaf(&self.input.commit().to_bytes()); + merkle::path_root(leaf, &self.path) + } +} + +/// An output to a partial transaction +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxOutputWitness { + pub output: OutputWitness, + pub path: Vec, +} + +impl PartialTxOutputWitness { + pub fn output_root(&self) -> [u8; 32] { + let leaf = merkle::leaf(&self.output.commit().to_bytes()); + merkle::path_root(leaf, &self.path) + } +} + +#[cfg(test)] +mod test { + + // use crate::cl::{ + // balance::UnitBalance, + // note::{derive_unit, NoteWitness}, + // nullifier::NullifierSecret, + // }; + + // use super::*; + + // #[test] + // fn test_partial_tx_balance() { + // let (nmo, eth, crv) = (derive_unit("NMO"), derive_unit("ETH"), derive_unit("CRV")); + // let mut rng = rand::thread_rng(); + + // 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()); + // let nmo_10 = InputWitness::from_output(nmo_10_utxo, nf_a); + + // let eth_23_utxo = OutputWitness::new(NoteWitness::basic(23, eth, &mut rng), nf_b.commit()); + // let eth_23 = InputWitness::from_output(eth_23_utxo, nf_b); + + // let crv_4840 = OutputWitness::new(NoteWitness::basic(4840, crv, &mut rng), nf_c.commit()); + + // let ptx_witness = PartialTxWitness { + // inputs: vec![nmo_10, eth_23], + // outputs: vec![crv_4840], + // balance_blinding: BalanceWitness::random_blinding(&mut rng), + // }; + + // let ptx = ptx_witness.commit(); + + // assert_eq!( + // ptx.balance, + // BalanceWitness { + // balances: vec![ + // UnitBalance { + // unit: nmo, + // pos: 0, + // neg: 10 + // }, + // UnitBalance { + // unit: eth, + // pos: 0, + // neg: 23 + // }, + // UnitBalance { + // unit: crv, + // pos: 4840, + // neg: 0 + // }, + // ], + // blinding: ptx_witness.balance_blinding + // } + // .commit() + // ); + // } +} diff --git a/emmarin/cl/cl/src/lib.rs b/emmarin/cl/cl/src/lib.rs new file mode 100644 index 0000000..9bd83c5 --- /dev/null +++ b/emmarin/cl/cl/src/lib.rs @@ -0,0 +1,2 @@ +pub mod cl; +pub mod zone_layer; diff --git a/emmarin/cl/cl/src/zone_layer/ledger.rs b/emmarin/cl/cl/src/zone_layer/ledger.rs new file mode 100644 index 0000000..43d476b --- /dev/null +++ b/emmarin/cl/cl/src/zone_layer/ledger.rs @@ -0,0 +1,34 @@ +use crate::cl::{merkle, mmr::MMR, Nullifier}; +use serde::{Deserialize, Serialize}; + +const MAX_NULL: usize = 256; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Ledger { + cm_root: [u8; 32], + nf_root: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerWitness { + pub commitments: MMR, + pub nullifiers: Vec, +} + +impl LedgerWitness { + pub fn commit(&self) -> Ledger { + Ledger { + cm_root: self.commitments.commit(), + 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)) + } +} diff --git a/emmarin/cl/cl/src/zone_layer/mod.rs b/emmarin/cl/cl/src/zone_layer/mod.rs new file mode 100644 index 0000000..20710b3 --- /dev/null +++ b/emmarin/cl/cl/src/zone_layer/mod.rs @@ -0,0 +1,3 @@ +pub mod ledger; +pub mod notes; +pub mod tx; diff --git a/emmarin/cl/cl/src/zone_layer/notes.rs b/emmarin/cl/cl/src/zone_layer/notes.rs new file mode 100644 index 0000000..8adf0e4 --- /dev/null +++ b/emmarin/cl/cl/src/zone_layer/notes.rs @@ -0,0 +1,14 @@ +use super::ledger::Ledger; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct ZoneNote { + pub stf: Stf, + pub state: State, + pub ledger: Ledger, + pub id: [u8; 32], +} + +pub type Stf = [u8; 32]; +pub type ZoneId = [u8; 32]; +pub type State = [u8; 32]; diff --git a/emmarin/cl/cl/src/zone_layer/tx.rs b/emmarin/cl/cl/src/zone_layer/tx.rs new file mode 100644 index 0000000..02bb511 --- /dev/null +++ b/emmarin/cl/cl/src/zone_layer/tx.rs @@ -0,0 +1,24 @@ +use super::notes::ZoneNote; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UpdateBundle { + pub updates: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ZoneUpdate { + pub old: ZoneNote, + pub new: ZoneNote, +} + +impl ZoneUpdate { + pub fn new(old: ZoneNote, new: ZoneNote) -> Self { + assert_eq!(old.id, new.id); + Self { old, new } + } + + pub fn well_formed(&self) -> bool { + self.old.id == self.new.id + } +} diff --git a/emmarin/cl/cl/tests/simple_transfer.rs b/emmarin/cl/cl/tests/simple_transfer.rs new file mode 100644 index 0000000..fca1b91 --- /dev/null +++ b/emmarin/cl/cl/tests/simple_transfer.rs @@ -0,0 +1,43 @@ +use cl::{ + cl::{ + note::derive_unit, BalanceWitness, InputWitness, NoteWitness, NullifierCommitment, + NullifierSecret, OutputWitness, PartialTxWitness, + }, + zone_layer::notes::ZoneId, +}; + +fn receive_utxo(note: NoteWitness, nf_pk: NullifierCommitment, zone_id: ZoneId) -> OutputWitness { + OutputWitness::new(note, nf_pk, zone_id) +} + +#[test] +fn test_simple_transfer() { + let nmo = derive_unit("NMO"); + let mut rng = rand::thread_rng(); + let zone_id = [0; 32]; + + let sender_nf_sk = NullifierSecret::random(&mut rng); + let sender_nf_pk = sender_nf_sk.commit(); + + let recipient_nf_pk = NullifierSecret::random(&mut rng).commit(); + + // Assume the sender has received an unspent output from somewhere + let utxo = receive_utxo(NoteWitness::basic(10, nmo, &mut rng), sender_nf_pk, zone_id); + + // and wants to send 8 NMO to some recipient and return 2 NMO to itself. + let recipient_output = OutputWitness::new( + NoteWitness::basic(8, nmo, &mut rng), + recipient_nf_pk, + zone_id, + ); + let change_output = + OutputWitness::new(NoteWitness::basic(2, nmo, &mut rng), sender_nf_pk, zone_id); + + let ptx_witness = PartialTxWitness { + inputs: vec![InputWitness::from_output(utxo, sender_nf_sk)], + outputs: vec![recipient_output, change_output], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + assert!(ptx_witness.balance().is_zero()) +} diff --git a/emmarin/cl/ledger/Cargo.toml b/emmarin/cl/ledger/Cargo.toml new file mode 100644 index 0000000..053c813 --- /dev/null +++ b/emmarin/cl/ledger/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2021" + +[dependencies] +cl = { path = "../cl" } +ledger_proof_statements = { path = "../ledger_proof_statements" } +nomos_cl_risc0_proofs = { path = "../risc0_proofs" } +ledger_validity_proof = { path = "../ledger_validity_proof" } +risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } +risc0-groth16 = { version = "1.0" } +rand = "0.8.5" +rand_core = "0.6.0" +thiserror = "1.0.62" +sha2 = "0.10" diff --git a/emmarin/cl/ledger/src/balance.rs b/emmarin/cl/ledger/src/balance.rs new file mode 100644 index 0000000..d408782 --- /dev/null +++ b/emmarin/cl/ledger/src/balance.rs @@ -0,0 +1,57 @@ +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/constraint.rs b/emmarin/cl/ledger/src/constraint.rs new file mode 100644 index 0000000..f6d4e5f --- /dev/null +++ b/emmarin/cl/ledger/src/constraint.rs @@ -0,0 +1,75 @@ +use cl::cl::{Constraint, Nullifier, PtxRoot}; +use ledger_proof_statements::constraint::ConstraintPublic; + +use crate::error::Result; + +#[derive(Debug, Clone)] +pub struct ConstraintProof { + pub risc0_id: [u32; 8], + pub risc0_receipt: risc0_zkvm::Receipt, +} + +pub fn risc0_constraint(risc0_id: [u32; 8]) -> Constraint { + unsafe { Constraint(core::mem::transmute::<[u32; 8], [u8; 32]>(risc0_id)) } +} + +impl ConstraintProof { + pub fn from_risc0(risc0_id: [u32; 8], risc0_receipt: risc0_zkvm::Receipt) -> Self { + Self { + risc0_id, + risc0_receipt, + } + } + + pub fn constraint(&self) -> Constraint { + risc0_constraint(self.risc0_id) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self, expected_public: ConstraintPublic) -> bool { + let Ok(public) = self.public() else { + return false; + }; + + expected_public == public && self.risc0_receipt.verify(self.risc0_id).is_ok() + } + + pub fn nop_constraint() -> Constraint { + risc0_constraint(nomos_cl_risc0_proofs::CONSTRAINT_NOP_ID) + } + + pub fn prove_nop(nf: Nullifier, ptx_root: PtxRoot) -> Self { + let constraint_public = ConstraintPublic { nf, ptx_root }; + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&constraint_public) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // 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::CONSTRAINT_NOP_ELF, &opts) + .unwrap(); + + println!( + "STARK 'constraint-nop' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + // extract the receipt. + let receipt = prove_info.receipt; + + Self::from_risc0(nomos_cl_risc0_proofs::CONSTRAINT_NOP_ID, receipt) + } +} diff --git a/emmarin/cl/ledger/src/error.rs b/emmarin/cl/ledger/src/error.rs new file mode 100644 index 0000000..4431a25 --- /dev/null +++ b/emmarin/cl/ledger/src/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +pub type Result = core::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error("risc0 failed to serde")] + Risc0Serde(#[from] risc0_zkvm::serde::Error), + #[error("risc0 failed to prove execution of the zkvm")] + Risc0ProofFailed, +} diff --git a/emmarin/cl/ledger/src/ledger.rs b/emmarin/cl/ledger/src/ledger.rs new file mode 100644 index 0000000..4875ba8 --- /dev/null +++ b/emmarin/cl/ledger/src/ledger.rs @@ -0,0 +1,100 @@ +use ledger_proof_statements::{ + ledger::{LedgerProofPrivate, LedgerProofPublic}, + ptx::PtxPublic, +}; + +use crate::{ + balance::ProvedBalance, + constraint::ConstraintProof, + error::{Error, Result}, + partial_tx::ProvedPartialTx, +}; +use cl::zone_layer::{ledger::LedgerWitness, 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, + id: zone_id, + }; + + let mut env = risc0_zkvm::ExecutorEnv::builder(); + + for bundle in bundles { + for proof in bundle.proofs() { + env.add_assumption(proof); + } + } + for covenant in constraints { + env.add_assumption(covenant.risc0_receipt); + } + let env = env.write(&witness).unwrap().build().unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // 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, ledger_validity_proof::LEDGER_ELF, &opts) + .map_err(|e| { + eprintln!("{e}"); + Error::Risc0ProofFailed + })?; + + println!( + "STARK 'ledger' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + Ok(Self { + public: prove_info + .receipt + .journal + .decode::() + .unwrap(), + risc0_receipt: prove_info.receipt, + }) + } + + pub fn verify(&self) -> bool { + self.risc0_receipt + .verify(ledger_validity_proof::LEDGER_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/src/lib.rs b/emmarin/cl/ledger/src/lib.rs new file mode 100644 index 0000000..d42f46c --- /dev/null +++ b/emmarin/cl/ledger/src/lib.rs @@ -0,0 +1,9 @@ +pub mod balance; +pub mod constraint; +pub mod error; +pub mod ledger; +pub mod partial_tx; +pub mod stf; +pub mod zone_update; + +pub use constraint::ConstraintProof; diff --git a/emmarin/cl/ledger/src/partial_tx.rs b/emmarin/cl/ledger/src/partial_tx.rs new file mode 100644 index 0000000..0566f2a --- /dev/null +++ b/emmarin/cl/ledger/src/partial_tx.rs @@ -0,0 +1,59 @@ +use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; + +use crate::error::{Error, Result}; +use cl::cl::{merkle, 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]>, + ) -> Result { + let ptx_private = PtxPrivate { + ptx: ptx_witness, + input_cm_paths, + cm_roots: cm_roots.clone(), + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&ptx_private) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = risc0_zkvm::default_prover(); + + let start_t = std::time::Instant::now(); + + // Proof information by proving the specified ELF binary. + // 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) + .map_err(|_| Error::Risc0ProofFailed)?; + + println!( + "STARK 'ptx' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + Ok(Self { + public: prove_info.receipt.journal.decode()?, + risc0_receipt: prove_info.receipt, + }) + } + + pub fn verify(&self) -> bool { + self.risc0_receipt + .verify(nomos_cl_risc0_proofs::PTX_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/src/stf.rs b/emmarin/cl/ledger/src/stf.rs new file mode 100644 index 0000000..d826503 --- /dev/null +++ b/emmarin/cl/ledger/src/stf.rs @@ -0,0 +1,63 @@ +use cl::zone_layer::notes::Stf; +use ledger_proof_statements::stf::StfPublic; + +#[derive(Debug, Clone)] +pub struct StfProof { + pub risc0_id: [u32; 8], + pub public: StfPublic, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +pub fn risc0_constraint(risc0_id: [u32; 8]) -> Stf { + // TODO: hash + + unsafe { core::mem::transmute::<[u32; 8], [u8; 32]>(risc0_id) } +} + +impl StfProof { + pub fn from_risc0(risc0_id: [u32; 8], risc0_receipt: risc0_zkvm::Receipt) -> Self { + Self { + risc0_id, + public: risc0_receipt.journal.decode().unwrap(), + risc0_receipt, + } + } + + pub fn stf(&self) -> Stf { + risc0_constraint(self.risc0_id) + } + pub fn verify(&self) -> bool { + self.risc0_receipt.verify(self.risc0_id).is_ok() + } + + pub fn prove_nop(public: StfPublic) -> Self { + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&public) + .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::STF_NOP_ELF, &opts) + .unwrap(); + + println!( + "STARK 'stf' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + let receipt = prove_info.receipt; + + Self { + risc0_id: nomos_cl_risc0_proofs::STF_NOP_ID, + public, + risc0_receipt: receipt, + } + } +} diff --git a/emmarin/cl/ledger/src/zone_update.rs b/emmarin/cl/ledger/src/zone_update.rs new file mode 100644 index 0000000..2bf7baa --- /dev/null +++ b/emmarin/cl/ledger/src/zone_update.rs @@ -0,0 +1,65 @@ +pub use crate::error::{Error, Result}; +use crate::{ledger::ProvedLedgerTransition, stf::StfProof}; +use cl::zone_layer::tx::UpdateBundle; +use std::collections::{HashMap, HashSet}; + +pub struct ProvedUpdateBundle { + pub bundle: UpdateBundle, + pub ledger_proofs: Vec, + pub stf_proofs: Vec, +} + +impl ProvedUpdateBundle { + pub fn verify(&self) -> bool { + let mut expected_zones = HashMap::new(); + let mut actual_zones = HashMap::new(); + for proof in &self.ledger_proofs { + if !proof.verify() { + return false; + } + + 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); + } + } + + println!("{:?} | {:?}", expected_zones, actual_zones); + for (bundle, expected) in expected_zones.iter() { + if let Some(actual) = actual_zones.get(bundle) { + if actual != expected { + panic!("{:?} | {:?}", actual, expected); + } + } else { + panic!(); + } + } + + for ((update, stf_proof), ledger_proof) in self + .bundle + .updates + .iter() + .zip(self.stf_proofs.iter()) + .zip(self.ledger_proofs.iter()) + { + if !update.well_formed() { + return false; + } + + if ledger_proof.public.old_ledger != update.old.ledger + || ledger_proof.public.ledger != update.new.ledger + { + return false; + } + + if stf_proof.public.old != update.old || stf_proof.public.new != update.new { + return false; + } + } + + true + } +} diff --git a/emmarin/cl/ledger/tests/simple_transfer.rs b/emmarin/cl/ledger/tests/simple_transfer.rs new file mode 100644 index 0000000..03cf9af --- /dev/null +++ b/emmarin/cl/ledger/tests/simple_transfer.rs @@ -0,0 +1,220 @@ +use cl::{ + cl::{ + balance::Unit, merkle, mmr::MMR, note::derive_unit, BalanceWitness, InputWitness, + NoteWitness, NullifierCommitment, NullifierSecret, OutputWitness, PartialTxWitness, + }, + zone_layer::{ + ledger::LedgerWitness, + notes::{ZoneId, ZoneNote}, + tx::{UpdateBundle, ZoneUpdate}, + }, +}; +use ledger::{ + balance::ProvedBalance, + constraint::ConstraintProof, + ledger::{ProvedBundle, ProvedLedgerTransition}, + partial_tx::ProvedPartialTx, + stf::StfProof, + zone_update::ProvedUpdateBundle, +}; +use ledger_proof_statements::{balance::BalancePrivate, stf::StfPublic}; +use rand_core::CryptoRngCore; +use std::sync::OnceLock; + +fn nmo() -> &'static Unit { + static NMO: OnceLock = OnceLock::new(); + NMO.get_or_init(|| derive_unit("NMO")) +} + +struct User(NullifierSecret); + +impl User { + fn random(mut rng: impl CryptoRngCore) -> Self { + Self(NullifierSecret::random(&mut rng)) + } + + fn pk(&self) -> NullifierCommitment { + self.0.commit() + } + + fn sk(&self) -> NullifierSecret { + self.0 + } +} + +fn receive_utxo(note: NoteWitness, nf_pk: NullifierCommitment, zone_id: ZoneId) -> OutputWitness { + OutputWitness::new(note, nf_pk, zone_id) +} + +fn cross_transfer_transition( + input: InputWitness, + input_path: Vec, + to: User, + amount: u64, + zone_a: ZoneId, + zone_b: ZoneId, + mut ledger_a: LedgerWitness, + mut ledger_b: LedgerWitness, +) -> (ProvedLedgerTransition, ProvedLedgerTransition) { + let mut rng = rand::thread_rng(); + assert!(amount <= input.note.value); + let change = input.note.value - amount; + let transfer = OutputWitness::new( + NoteWitness::basic(amount, *nmo(), &mut rng), + to.pk(), + zone_b, + ); + let change = OutputWitness::new( + NoteWitness::basic(change, *nmo(), &mut rng), + input.nf_sk.commit(), + zone_a, + ); + + // Construct the ptx consuming the input and producing the two outputs. + let ptx_witness = PartialTxWitness { + inputs: vec![input], + 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()); + + let ledger_a_transition = ProvedLedgerTransition::prove( + ledger_a.clone(), + zone_a, + vec![zone_tx.clone()], + vec![constraint_proof], + ) + .unwrap(); + + let ledger_b_transition = + ProvedLedgerTransition::prove(ledger_b.clone(), zone_b, vec![zone_tx], vec![]).unwrap(); + + ledger_a.commitments.push(&change.commit_note().0); + ledger_a.nullifiers.push(input.nullifier()); + + ledger_b.commitments.push(&transfer.commit_note().0); + + assert_eq!(ledger_a_transition.public.ledger, ledger_a.commit()); + assert_eq!(ledger_b_transition.public.ledger, ledger_b.commit()); + + (ledger_a_transition, ledger_b_transition) +} + +#[test] +fn zone_update_cross() { + let mut rng = rand::thread_rng(); + + let zone_a_id = [0; 32]; + let zone_b_id = [1; 32]; + + // alice is sending 8 NMO to bob. + + let alice = User::random(&mut rng); + let bob = User::random(&mut rng); + + // Alice has an unspent note worth 10 NMO + let utxo = receive_utxo( + 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 ledger_a = LedgerWitness { + commitments: mmr, + nullifiers: vec![], + }; + + let ledger_b = LedgerWitness { + commitments: MMR::new(), + nullifiers: vec![], + }; + + let zone_a_old = ZoneNote { + id: zone_a_id, + state: [0; 32], + ledger: ledger_a.commit(), + stf: [0; 32], + }; + let zone_b_old = ZoneNote { + id: zone_b_id, + state: [0; 32], + ledger: ledger_b.commit(), + stf: [0; 32], + }; + + let (ledger_a_transition, ledger_b_transition) = cross_transfer_transition( + alice_input, + input_cm_path, + bob, + 8, + zone_a_id, + zone_b_id, + ledger_a, + ledger_b, + ); + + let zone_a_new = ZoneNote { + ledger: ledger_a_transition.public.ledger, + ..zone_a_old + }; + + let zone_b_new = ZoneNote { + ledger: ledger_b_transition.public.ledger, + ..zone_b_old + }; + + let stf_proof_a = StfProof::prove_nop(StfPublic { + old: zone_a_old, + new: zone_a_new, + }); + + let stf_proof_b = StfProof::prove_nop(StfPublic { + old: zone_b_old, + new: zone_b_new, + }); + + let update_bundle = UpdateBundle { + updates: vec![ + ZoneUpdate { + old: zone_a_old, + new: zone_a_new, + }, + ZoneUpdate { + old: zone_b_old, + new: zone_b_new, + }, + ], + }; + + let proved_bundle = ProvedUpdateBundle { + bundle: update_bundle, + ledger_proofs: vec![ledger_a_transition, ledger_b_transition], + stf_proofs: vec![stf_proof_a, stf_proof_b], + }; + + assert!(proved_bundle.verify()); +} diff --git a/emmarin/cl/ledger_proof_statements/Cargo.toml b/emmarin/cl/ledger_proof_statements/Cargo.toml new file mode 100644 index 0000000..65ea695 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ledger_proof_statements" +version = "0.1.0" +edition = "2021" + +[dependencies] +cl = { path = "../cl" } +serde = { version = "1.0", features = ["derive"] } diff --git a/emmarin/cl/ledger_proof_statements/src/balance.rs b/emmarin/cl/ledger_proof_statements/src/balance.rs new file mode 100644 index 0000000..509b1b5 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/balance.rs @@ -0,0 +1,12 @@ +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/constraint.rs b/emmarin/cl/ledger_proof_statements/src/constraint.rs new file mode 100644 index 0000000..0a29aa6 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/constraint.rs @@ -0,0 +1,8 @@ +use cl::cl::{Nullifier, PtxRoot}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConstraintPublic { + pub nf: Nullifier, + pub ptx_root: PtxRoot, +} diff --git a/emmarin/cl/ledger_proof_statements/src/ledger.rs b/emmarin/cl/ledger_proof_statements/src/ledger.rs new file mode 100644 index 0000000..a7feda6 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/ledger.rs @@ -0,0 +1,29 @@ +use crate::ptx::PtxPublic; +use cl::cl::{bundle::BundleId, Output}; +use cl::zone_layer::{ + ledger::{Ledger, LedgerWitness}, + notes::ZoneId, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerProofPublic { + pub old_ledger: Ledger, + pub ledger: Ledger, + pub id: ZoneId, + pub cross_bundles: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerProofPrivate { + pub ledger: LedgerWitness, + pub id: ZoneId, + pub bundles: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CrossZoneBundle { + pub id: BundleId, + pub zones: Vec, +} diff --git a/emmarin/cl/ledger_proof_statements/src/lib.rs b/emmarin/cl/ledger_proof_statements/src/lib.rs new file mode 100644 index 0000000..2000d4b --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/lib.rs @@ -0,0 +1,5 @@ +pub mod balance; +pub mod constraint; +pub mod ledger; +pub mod ptx; +pub mod stf; diff --git a/emmarin/cl/ledger_proof_statements/src/ptx.rs b/emmarin/cl/ledger_proof_statements/src/ptx.rs new file mode 100644 index 0000000..93d555c --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/ptx.rs @@ -0,0 +1,15 @@ +use cl::cl::{merkle, 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]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PtxPrivate { + pub ptx: PartialTxWitness, + pub input_cm_paths: Vec>, + pub cm_roots: Vec<[u8; 32]>, +} diff --git a/emmarin/cl/ledger_proof_statements/src/stf.rs b/emmarin/cl/ledger_proof_statements/src/stf.rs new file mode 100644 index 0000000..22eb9cc --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/stf.rs @@ -0,0 +1,8 @@ +use cl::zone_layer::notes::ZoneNote; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct StfPublic { + pub old: ZoneNote, + pub new: ZoneNote, +} diff --git a/emmarin/cl/ledger_validity_proof/Cargo.toml b/emmarin/cl/ledger_validity_proof/Cargo.toml new file mode 100644 index 0000000..b3a8d5f --- /dev/null +++ b/emmarin/cl/ledger_validity_proof/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ledger_validity_proof" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = [ "ledger"] + diff --git a/emmarin/cl/ledger_validity_proof/build.rs b/emmarin/cl/ledger_validity_proof/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/emmarin/cl/ledger_validity_proof/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml b/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml new file mode 100644 index 0000000..a05b1fa --- /dev/null +++ b/emmarin/cl/ledger_validity_proof/ledger/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +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" } + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/ledger_validity_proof/ledger/src/main.rs b/emmarin/cl/ledger_validity_proof/ledger/src/main.rs new file mode 100644 index 0000000..5849b15 --- /dev/null +++ b/emmarin/cl/ledger_validity_proof/ledger/src/main.rs @@ -0,0 +1,109 @@ +use cl::{ + cl::{Bundle, Output}, + zone_layer::{ledger::LedgerWitness, notes::ZoneId}, +}; +use ledger_proof_statements::{ + balance::BalancePublic, + constraint::ConstraintPublic, + ledger::{CrossZoneBundle, LedgerProofPrivate, LedgerProofPublic}, + ptx::PtxPublic, +}; +use risc0_zkvm::{guest::env, serde}; + +fn main() { + let LedgerProofPrivate { + mut ledger, + id, + bundles, + } = env::read(); + + let old_ledger = ledger.commit(); + let mut cross_bundles = vec![]; + let mut outputs = vec![]; + + let roots = ledger + .commitments + .roots + .iter() + .map(|r| r.root) + .collect::>(); + + 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(); + + for ptx in &bundle { + let (new_ledger, ptx_outputs) = process_ptx(ledger, ptx, id, &roots); + ledger = new_ledger; + outputs.extend(ptx_outputs); + } + + 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(), + }); + } + } + + env::commit(&LedgerProofPublic { + old_ledger, + 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/ledger_validity_proof/src/lib.rs b/emmarin/cl/ledger_validity_proof/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/emmarin/cl/ledger_validity_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 new file mode 100644 index 0000000..19d562a --- /dev/null +++ b/emmarin/cl/risc0_proofs/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nomos_cl_risc0_proofs" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "1.0" } + +[package.metadata.risc0] +methods = ["balance", "constraint_nop", "ptx", "stf_nop"] + diff --git a/emmarin/cl/risc0_proofs/balance/Cargo.toml b/emmarin/cl/risc0_proofs/balance/Cargo.toml new file mode 100644 index 0000000..161f1c8 --- /dev/null +++ b/emmarin/cl/risc0_proofs/balance/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "balance" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +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" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/balance/src/main.rs b/emmarin/cl/risc0_proofs/balance/src/main.rs new file mode 100644 index 0000000..3342001 --- /dev/null +++ b/emmarin/cl/risc0_proofs/balance/src/main.rs @@ -0,0 +1,24 @@ +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/build.rs b/emmarin/cl/risc0_proofs/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/emmarin/cl/risc0_proofs/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml b/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml new file mode 100644 index 0000000..1aac242 --- /dev/null +++ b/emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "constraint_nop" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +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" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs b/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs new file mode 100644 index 0000000..25d9c0c --- /dev/null +++ b/emmarin/cl/risc0_proofs/constraint_nop/src/main.rs @@ -0,0 +1,8 @@ +/// Constraint No-op Proof +use ledger_proof_statements::constraint::ConstraintPublic; +use risc0_zkvm::guest::env; + +fn main() { + let public: ConstraintPublic = env::read(); + env::commit(&public); +} diff --git a/emmarin/cl/risc0_proofs/ptx/Cargo.toml b/emmarin/cl/risc0_proofs/ptx/Cargo.toml new file mode 100644 index 0000000..181aef7 --- /dev/null +++ b/emmarin/cl/risc0_proofs/ptx/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ptx" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +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" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/ptx/src/main.rs b/emmarin/cl/risc0_proofs/ptx/src/main.rs new file mode 100644 index 0000000..7b03f68 --- /dev/null +++ b/emmarin/cl/risc0_proofs/ptx/src/main.rs @@ -0,0 +1,28 @@ +/// 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, + }); +} diff --git a/emmarin/cl/risc0_proofs/src/lib.rs b/emmarin/cl/risc0_proofs/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/emmarin/cl/risc0_proofs/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/emmarin/cl/risc0_proofs/stf_nop/Cargo.toml b/emmarin/cl/risc0_proofs/stf_nop/Cargo.toml new file mode 100644 index 0000000..ec1e0aa --- /dev/null +++ b/emmarin/cl/risc0_proofs/stf_nop/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "stf_nop" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +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" } + + +[patch.crates-io] +# add RISC Zero accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +curve25519-dalek = { git = "https://github.com/risc0/curve25519-dalek", tag = "curve25519-4.1.2-risczero.0" } diff --git a/emmarin/cl/risc0_proofs/stf_nop/src/main.rs b/emmarin/cl/risc0_proofs/stf_nop/src/main.rs new file mode 100644 index 0000000..7c10c92 --- /dev/null +++ b/emmarin/cl/risc0_proofs/stf_nop/src/main.rs @@ -0,0 +1,8 @@ +/// Constraint No-op Proof +use ledger_proof_statements::stf::StfPublic; +use risc0_zkvm::guest::env; + +fn main() { + let public: StfPublic = env::read(); + env::commit(&public); +}