From ec5cd13d46c9cba1b1dad36ce474df214b5e0117 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Mon, 18 Nov 2024 14:07:04 +0100 Subject: [PATCH] emmarin --- emmarin/cl/.gitignore | 2 + emmarin/cl/Cargo.toml | 11 + emmarin/cl/cl/Cargo.toml | 15 ++ emmarin/cl/cl/src/balance.rs | 149 +++++++++++ emmarin/cl/cl/src/bundle.rs | 117 +++++++++ emmarin/cl/cl/src/crypto.rs | 6 + emmarin/cl/cl/src/error.rs | 4 + emmarin/cl/cl/src/input.rs | 85 +++++++ emmarin/cl/cl/src/lib.rs | 21 ++ emmarin/cl/cl/src/merkle.rs | 239 ++++++++++++++++++ emmarin/cl/cl/src/note.rs | 171 +++++++++++++ emmarin/cl/cl/src/nullifier.rs | 161 ++++++++++++ emmarin/cl/cl/src/output.rs | 45 ++++ emmarin/cl/cl/src/partial_tx.rs | 217 ++++++++++++++++ emmarin/cl/cl/src/zones.rs | 100 ++++++++ emmarin/cl/cl/tests/simple_transfer.rs | 37 +++ emmarin/cl/ledger/Cargo.toml | 16 ++ emmarin/cl/ledger/src/bundle.rs | 67 +++++ emmarin/cl/ledger/src/constraint.rs | 75 ++++++ emmarin/cl/ledger/src/error.rs | 11 + emmarin/cl/ledger/src/ledger.rs | 128 ++++++++++ emmarin/cl/ledger/src/lib.rs | 8 + emmarin/cl/ledger/src/pact.rs | 75 ++++++ emmarin/cl/ledger/src/partial_tx.rs | 77 ++++++ emmarin/cl/ledger/tests/simple_transfer.rs | 126 +++++++++ emmarin/cl/ledger_proof_statements/Cargo.toml | 8 + .../cl/ledger_proof_statements/src/bundle.rs | 12 + .../ledger_proof_statements/src/constraint.rs | 8 + .../cl/ledger_proof_statements/src/ledger.rs | 30 +++ emmarin/cl/ledger_proof_statements/src/lib.rs | 5 + .../cl/ledger_proof_statements/src/pact.rs | 15 ++ emmarin/cl/ledger_proof_statements/src/ptx.rs | 16 ++ emmarin/cl/ledger_validity_proof/Cargo.toml | 11 + emmarin/cl/ledger_validity_proof/build.rs | 3 + .../ledger_validity_proof/ledger/Cargo.toml | 19 ++ .../ledger_validity_proof/ledger/src/main.rs | 146 +++++++++++ emmarin/cl/ledger_validity_proof/src/lib.rs | 1 + emmarin/cl/risc0_proofs/Cargo.toml | 11 + emmarin/cl/risc0_proofs/build.rs | 3 + emmarin/cl/risc0_proofs/bundle/Cargo.toml | 19 ++ emmarin/cl/risc0_proofs/bundle/src/main.rs | 23 ++ .../cl/risc0_proofs/constraint_nop/Cargo.toml | 19 ++ .../risc0_proofs/constraint_nop/src/main.rs | 8 + emmarin/cl/risc0_proofs/pact/Cargo.toml | 19 ++ emmarin/cl/risc0_proofs/pact/src/main.rs | 30 +++ emmarin/cl/risc0_proofs/ptx/Cargo.toml | 19 ++ emmarin/cl/risc0_proofs/ptx/src/main.rs | 29 +++ emmarin/cl/risc0_proofs/src/lib.rs | 1 + 48 files changed, 2418 insertions(+) create mode 100644 emmarin/cl/.gitignore create mode 100644 emmarin/cl/Cargo.toml create mode 100644 emmarin/cl/cl/Cargo.toml create mode 100644 emmarin/cl/cl/src/balance.rs create mode 100644 emmarin/cl/cl/src/bundle.rs create mode 100644 emmarin/cl/cl/src/crypto.rs create mode 100644 emmarin/cl/cl/src/error.rs create mode 100644 emmarin/cl/cl/src/input.rs create mode 100644 emmarin/cl/cl/src/lib.rs create mode 100644 emmarin/cl/cl/src/merkle.rs create mode 100644 emmarin/cl/cl/src/note.rs create mode 100644 emmarin/cl/cl/src/nullifier.rs create mode 100644 emmarin/cl/cl/src/output.rs create mode 100644 emmarin/cl/cl/src/partial_tx.rs create mode 100644 emmarin/cl/cl/src/zones.rs create mode 100644 emmarin/cl/cl/tests/simple_transfer.rs create mode 100644 emmarin/cl/ledger/Cargo.toml create mode 100644 emmarin/cl/ledger/src/bundle.rs create mode 100644 emmarin/cl/ledger/src/constraint.rs create mode 100644 emmarin/cl/ledger/src/error.rs create mode 100644 emmarin/cl/ledger/src/ledger.rs create mode 100644 emmarin/cl/ledger/src/lib.rs create mode 100644 emmarin/cl/ledger/src/pact.rs create mode 100644 emmarin/cl/ledger/src/partial_tx.rs create mode 100644 emmarin/cl/ledger/tests/simple_transfer.rs create mode 100644 emmarin/cl/ledger_proof_statements/Cargo.toml create mode 100644 emmarin/cl/ledger_proof_statements/src/bundle.rs create mode 100644 emmarin/cl/ledger_proof_statements/src/constraint.rs create mode 100644 emmarin/cl/ledger_proof_statements/src/ledger.rs create mode 100644 emmarin/cl/ledger_proof_statements/src/lib.rs create mode 100644 emmarin/cl/ledger_proof_statements/src/pact.rs create mode 100644 emmarin/cl/ledger_proof_statements/src/ptx.rs create mode 100644 emmarin/cl/ledger_validity_proof/Cargo.toml create mode 100644 emmarin/cl/ledger_validity_proof/build.rs create mode 100644 emmarin/cl/ledger_validity_proof/ledger/Cargo.toml create mode 100644 emmarin/cl/ledger_validity_proof/ledger/src/main.rs create mode 100644 emmarin/cl/ledger_validity_proof/src/lib.rs create mode 100644 emmarin/cl/risc0_proofs/Cargo.toml create mode 100644 emmarin/cl/risc0_proofs/build.rs create mode 100644 emmarin/cl/risc0_proofs/bundle/Cargo.toml create mode 100644 emmarin/cl/risc0_proofs/bundle/src/main.rs create mode 100644 emmarin/cl/risc0_proofs/constraint_nop/Cargo.toml create mode 100644 emmarin/cl/risc0_proofs/constraint_nop/src/main.rs create mode 100644 emmarin/cl/risc0_proofs/pact/Cargo.toml create mode 100644 emmarin/cl/risc0_proofs/pact/src/main.rs create mode 100644 emmarin/cl/risc0_proofs/ptx/Cargo.toml create mode 100644 emmarin/cl/risc0_proofs/ptx/src/main.rs create mode 100644 emmarin/cl/risc0_proofs/src/lib.rs 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..b73749b --- /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" diff --git a/emmarin/cl/cl/src/balance.rs b/emmarin/cl/cl/src/balance.rs new file mode 100644 index 0000000..10db851 --- /dev/null +++ b/emmarin/cl/cl/src/balance.rs @@ -0,0 +1,149 @@ +use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::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/bundle.rs b/emmarin/cl/cl/src/bundle.rs new file mode 100644 index 0000000..c54e888 --- /dev/null +++ b/emmarin/cl/cl/src/bundle.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; + +use crate::{partial_tx::PartialTx, BalanceWitness, PartialTxWitness}; + +/// 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, Serialize, Deserialize)] +pub struct Bundle { + pub partials: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundleWitness { + pub partials: Vec, +} + +impl BundleWitness { + pub fn balance(&self) -> BalanceWitness { + BalanceWitness::combine(self.partials.iter().map(|ptx| ptx.balance()), [0u8; 16]) + } + + // pub fn commit(&self) -> Bundle { + // Bundle { + // partials: Vec::from_iter(self.partials.iter().map(|ptx| ptx.commit())), + // } + // } +} + +#[cfg(test)] +mod test { + use crate::{ + balance::UnitBalance, + input::InputWitness, + note::{derive_unit, NoteWitness}, + nullifier::NullifierSecret, + output::OutputWitness, + partial_tx::PartialTxWitness, + }; + + use super::*; + + #[test] + fn test_bundle_balance() { + let mut rng = rand::thread_rng(); + 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()); + 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()); + 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()); + + let ptx_unbalanced = PartialTxWitness { + inputs: vec![nmo_10_in, eth_23_in], + outputs: vec![crv_4840_out], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let bundle_witness = BundleWitness { + partials: vec![ptx_unbalanced.clone()], + }; + + assert!(!bundle_witness.balance().is_zero()); + assert_eq!( + bundle_witness.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 + ); + let eth_23_out = OutputWitness::new( + NoteWitness::basic(23, eth, &mut rng), + NullifierSecret::random(&mut rng).commit(), // transferring to a random owner + ); + + 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 witness = BundleWitness { + partials: vec![ptx_unbalanced, ptx_solved], + }; + + assert!(witness.balance().is_zero()); + assert_eq!(witness.balance().balances, vec![]); + } +} diff --git a/emmarin/cl/cl/src/crypto.rs b/emmarin/cl/cl/src/crypto.rs new file mode 100644 index 0000000..de35b1b --- /dev/null +++ b/emmarin/cl/cl/src/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/error.rs b/emmarin/cl/cl/src/error.rs new file mode 100644 index 0000000..03d01d1 --- /dev/null +++ b/emmarin/cl/cl/src/error.rs @@ -0,0 +1,4 @@ +#[derive(Debug)] +pub enum Error { + ProofFailed, +} diff --git a/emmarin/cl/cl/src/input.rs b/emmarin/cl/cl/src/input.rs new file mode 100644 index 0000000..5253ce9 --- /dev/null +++ b/emmarin/cl/cl/src/input.rs @@ -0,0 +1,85 @@ +/// 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::{ + note::{Constraint, NoteWitness}, + nullifier::{Nullifier, NullifierSecret}, + Nonce, +}; +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, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct InputWitness { + pub note: NoteWitness, + pub nf_sk: NullifierSecret, +} + +impl InputWitness { + pub fn new(note: NoteWitness, nf_sk: NullifierSecret) -> Self { + Self { note, nf_sk } + } + + pub fn from_output(output: crate::OutputWitness, nf_sk: NullifierSecret) -> Self { + assert_eq!(nf_sk.commit(), output.nf_pk); + Self::new(output.note, nf_sk) + } + + pub fn public(output: crate::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) + } + + pub fn evolved_nonce(&self, tag: &dyn AsRef<[u8]>, 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(tag, self.nf_sk.commit()).0); + + let nonce_bytes: [u8; 32] = hasher.finalize().into(); + Nonce::from_bytes(nonce_bytes) + } + + pub fn evolve_output(&self, tag: &dyn AsRef<[u8]>, domain: &[u8]) -> crate::OutputWitness { + crate::OutputWitness { + note: NoteWitness { + nonce: self.evolved_nonce(tag, domain), + ..self.note + }, + nf_pk: self.nf_sk.commit(), + } + } + + pub fn nullifier(&self, tag: &dyn AsRef<[u8]>) -> Nullifier { + Nullifier::new(tag, self.nf_sk, self.note_commitment(tag)) + } + + pub fn commit(&self, tag: &dyn AsRef<[u8]>) -> Input { + Input { + nullifier: self.nullifier(tag), + constraint: self.note.constraint, + } + } + + pub fn note_commitment(&self, tag: &dyn AsRef<[u8]>) -> crate::NoteCommitment { + self.note.commit(tag, self.nf_sk.commit()) + } +} + +impl Input { + pub fn to_bytes(&self) -> [u8; 64] { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(self.nullifier.as_bytes()); + bytes[32..64].copy_from_slice(&self.constraint.0); + bytes + } +} diff --git a/emmarin/cl/cl/src/lib.rs b/emmarin/cl/cl/src/lib.rs new file mode 100644 index 0000000..fea07f5 --- /dev/null +++ b/emmarin/cl/cl/src/lib.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 note; +pub mod nullifier; +pub mod output; +pub mod partial_tx; +pub mod zones; + +pub use balance::{Balance, BalanceWitness}; +pub use bundle::{Bundle, BundleWitness}; +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/merkle.rs b/emmarin/cl/cl/src/merkle.rs new file mode 100644 index 0000000..bb10e3c --- /dev/null +++ b/emmarin/cl/cl/src/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/note.rs b/emmarin/cl/cl/src/note.rs new file mode 100644 index 0000000..96b3fed --- /dev/null +++ b/emmarin/cl/cl/src/note.rs @@ -0,0 +1,171 @@ +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{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(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::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/nullifier.rs b/emmarin/cl/cl/src/nullifier.rs new file mode 100644 index 0000000..c6ee27a --- /dev/null +++ b/emmarin/cl/cl/src/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::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::{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/output.rs b/emmarin/cl/cl/src/output.rs new file mode 100644 index 0000000..e6aed88 --- /dev/null +++ b/emmarin/cl/cl/src/output.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + note::{NoteCommitment, NoteWitness}, + nullifier::NullifierCommitment, + NullifierSecret, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Output { + pub note_comm: NoteCommitment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputWitness { + pub note: NoteWitness, + pub nf_pk: NullifierCommitment, +} + +impl OutputWitness { + pub fn new(note: NoteWitness, nf_pk: NullifierCommitment) -> Self { + Self { note, nf_pk } + } + + pub fn public(note: NoteWitness) -> Self { + let nf_pk = NullifierSecret::zero().commit(); + Self { note, nf_pk } + } + + pub fn commit_note(&self, tag: &dyn AsRef<[u8]>) -> NoteCommitment { + self.note.commit(tag, self.nf_pk) + } + + pub fn commit(&self, tag: &dyn AsRef<[u8]>) -> Output { + Output { + note_comm: self.commit_note(tag), + } + } +} + +impl Output { + pub fn to_bytes(&self) -> [u8; 32] { + self.note_comm.0 + } +} diff --git a/emmarin/cl/cl/src/partial_tx.rs b/emmarin/cl/cl/src/partial_tx.rs new file mode 100644 index 0000000..3d5fc97 --- /dev/null +++ b/emmarin/cl/cl/src/partial_tx.rs @@ -0,0 +1,217 @@ +use rand_core::{CryptoRngCore, RngCore}; +use serde::{Deserialize, Serialize}; + +use crate::balance::{Balance, BalanceWitness}; +use crate::input::{Input, InputWitness}; +use crate::merkle; +use crate::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, zone: &dyn AsRef<[u8]>) -> PartialTx { + PartialTx { + inputs: Vec::from_iter(self.inputs.iter().map(|i| i.commit(zone))), + outputs: Vec::from_iter(self.outputs.iter().map(|o| o.commit(zone))), + balance: self.balance().commit(), + } + } + + pub fn input_witness(&self, tag: &dyn AsRef<[u8]>, idx: usize) -> PartialTxInputWitness { + let input_bytes = Vec::from_iter( + self.inputs + .iter() + .map(|i| i.commit(tag).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, tag: &dyn AsRef<[u8]>, idx: usize) -> PartialTxOutputWitness { + let output_bytes = Vec::from_iter( + self.outputs + .iter() + .map(|o| o.commit(tag).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, tag: &dyn AsRef<[u8]>) -> [u8; 32] { + let leaf = merkle::leaf(&self.input.commit(tag).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, tag: &dyn AsRef<[u8]>) -> [u8; 32] { + let leaf = merkle::leaf(&self.output.commit(tag).to_bytes()); + merkle::path_root(leaf, &self.path) + } +} + +#[cfg(test)] +mod test { + + use crate::{ + 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/zones.rs b/emmarin/cl/cl/src/zones.rs new file mode 100644 index 0000000..ede32b8 --- /dev/null +++ b/emmarin/cl/cl/src/zones.rs @@ -0,0 +1,100 @@ +use crate::{merkle, Constraint, NoteCommitment, Nullifier, PartialTx, PartialTxWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Pact { + pub tx: PartialTx, + pub to: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PactWitness { + pub tx: PartialTxWitness, + pub from: ZoneId, + pub to: Vec, +} + +impl PactWitness { + pub fn commit(&self) -> Pact { + assert_eq!(self.tx.outputs.len(), self.to.len()); + let ptx = PartialTx { + inputs: Vec::from_iter(self.tx.inputs.iter().map(|i| i.commit(&self.from))), + outputs: Vec::from_iter( + self.tx + .outputs + .iter() + .zip(&self.to) + .map(|(o, z)| o.commit(z)), + ), + balance: self.tx.balance().commit(), + }; + Pact { + tx: ptx, + to: self.to.clone(), + } + } +} + +pub struct ZoneNote { + pub stf: Constraint, + pub state: State, + pub ledger: Ledger, + pub id: [u8; 32], +} + +pub type State = [u8; 32]; +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Ledger { + cm_root: [u8; 32], + nf_root: [u8; 32], +} + +pub type ZoneId = [u8; 32]; +pub struct StateWitness; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerWitness { + pub commitments: Vec, + pub nullifiers: Vec, +} + +const MAX_COMM: usize = 256; +const MAX_NULL: usize = 256; + +impl LedgerWitness { + pub fn commit(&self) -> Ledger { + Ledger { + cm_root: self.cm_root(), + 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)) + } + + pub fn cm_root(&self) -> [u8; 32] { + let bytes = self + .commitments + .iter() + .map(|i| i.as_bytes().to_vec()) + .collect::>(); + merkle::root(merkle::padded_leaves::(&bytes)) + } + + pub fn cm_path(&self, cm: &NoteCommitment) -> Option> { + let bytes = self + .commitments + .iter() + .map(|i| i.as_bytes().to_vec()) + .collect::>(); + let leaves = merkle::padded_leaves::(&bytes); + let idx = self.commitments.iter().position(|c| c == cm)?; + Some(merkle::path(leaves, idx)) + } +} diff --git a/emmarin/cl/cl/tests/simple_transfer.rs b/emmarin/cl/cl/tests/simple_transfer.rs new file mode 100644 index 0000000..f1a7a95 --- /dev/null +++ b/emmarin/cl/cl/tests/simple_transfer.rs @@ -0,0 +1,37 @@ +use cl::{note::derive_unit, BalanceWitness}; + +fn receive_utxo(note: cl::NoteWitness, nf_pk: cl::NullifierCommitment) -> cl::OutputWitness { + cl::OutputWitness::new(note, nf_pk) +} + +#[test] +fn test_simple_transfer() { + let nmo = derive_unit("NMO"); + let mut rng = rand::thread_rng(); + + let sender_nf_sk = cl::NullifierSecret::random(&mut rng); + let sender_nf_pk = sender_nf_sk.commit(); + + let recipient_nf_pk = cl::NullifierSecret::random(&mut rng).commit(); + + // Assume the sender has received an unspent output from somewhere + let utxo = receive_utxo(cl::NoteWitness::basic(10, nmo, &mut rng), sender_nf_pk); + + // and wants to send 8 NMO to some recipient and return 2 NMO to itself. + let recipient_output = + cl::OutputWitness::new(cl::NoteWitness::basic(8, nmo, &mut rng), recipient_nf_pk); + let change_output = + cl::OutputWitness::new(cl::NoteWitness::basic(2, nmo, &mut rng), sender_nf_pk); + + let ptx_witness = cl::PartialTxWitness { + inputs: vec![cl::InputWitness::from_output(utxo, sender_nf_sk)], + outputs: vec![recipient_output, change_output], + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }; + + let bundle = cl::BundleWitness { + partials: vec![ptx_witness], + }; + + assert!(bundle.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/bundle.rs b/emmarin/cl/ledger/src/bundle.rs new file mode 100644 index 0000000..17e8432 --- /dev/null +++ b/emmarin/cl/ledger/src/bundle.rs @@ -0,0 +1,67 @@ +use ledger_proof_statements::bundle::{BundlePrivate, BundlePublic}; + +use crate::error::{Error, Result}; + +pub struct ProvedBundle { + pub bundle: BundlePublic, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedBundle { + pub fn prove(bundle_witness: &cl::BundleWitness) -> Result { + // need to show that bundle is balanced. + // i.e. the sum of ptx balances is 0 + + let bundle_private = BundlePrivate { + balances: bundle_witness + .partials + .iter() + .map(|ptx| ptx.balance()) + .collect(), + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&bundle_private) + .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::BUNDLE_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::BUNDLE_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..4d03773 --- /dev/null +++ b/emmarin/cl/ledger/src/constraint.rs @@ -0,0 +1,75 @@ +use cl::Constraint; +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: cl::Nullifier, ptx_root: cl::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..9288916 --- /dev/null +++ b/emmarin/cl/ledger/src/ledger.rs @@ -0,0 +1,128 @@ +use ledger_proof_statements::ledger::{LedgerProofPrivate, LedgerProofPublic, ZoneTx}; + +use crate::{ + bundle::ProvedBundle, + constraint::ConstraintProof, + error::{Error, Result}, + pact::ProvedPact, + partial_tx::ProvedPartialTx, +}; +use cl::zones::{LedgerWitness, ZoneId}; + +pub struct ProvedLedgerTransition { + pub public: LedgerProofPublic, + pub risc0_receipt: risc0_zkvm::Receipt, +} + +pub enum ProvedZoneTx { + LocalTx { + bundle: ProvedBundle, + ptxs: Vec, + }, + Pact(ProvedPact), +} + +impl ProvedZoneTx { + fn to_public(&self) -> ZoneTx { + match self { + Self::LocalTx { ptxs, bundle } => ZoneTx::LocalTx { + ptxs: ptxs.iter().map(|p| p.public().unwrap()).collect(), + bundle: bundle.public().unwrap(), + }, + Self::Pact(pact) => ZoneTx::Pact(pact.public().unwrap()), + } + } + + fn proofs(&self) -> Vec { + match self { + Self::LocalTx { ptxs, bundle } => { + let mut proofs = vec![bundle.risc0_receipt.clone()]; + proofs.extend(ptxs.iter().map(|p| p.risc0_receipt.clone())); + proofs + } + Self::Pact(pact) => vec![pact.risc0_receipt.clone()], + } + } +} + +impl ProvedLedgerTransition { + pub fn prove( + ledger: LedgerWitness, + zone_id: ZoneId, + ptxs: Vec, + constraints: Vec, + ) -> Result { + let witness = LedgerProofPrivate { + txs: ptxs.iter().map(|p| p.to_public()).collect(), + ledger, + id: zone_id, + }; + + let mut env = risc0_zkvm::ExecutorEnv::builder(); + + for ptx in ptxs { + for proof in ptx.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 public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + // let Ok(proved_ptx_inputs) = self.public() else { + // return false; + // }; + // let expected_ptx_inputs = PtxPublic { + // ptx: self.ptx.clone(), + // cm_root: self.cm_root, + // from: self.from, + // to: self.to, + // }; + // if expected_ptx_inputs != proved_ptx_inputs { + // return false; + // } + + // let ptx_root = self.ptx.root(); + + 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..e95c59c --- /dev/null +++ b/emmarin/cl/ledger/src/lib.rs @@ -0,0 +1,8 @@ +pub mod bundle; +pub mod constraint; +pub mod error; +pub mod ledger; +pub mod pact; +pub mod partial_tx; + +pub use constraint::ConstraintProof; diff --git a/emmarin/cl/ledger/src/pact.rs b/emmarin/cl/ledger/src/pact.rs new file mode 100644 index 0000000..50ee756 --- /dev/null +++ b/emmarin/cl/ledger/src/pact.rs @@ -0,0 +1,75 @@ +use crate::error::{Error, Result}; +use cl::zones::*; +use ledger_proof_statements::pact::{PactPrivate, PactPublic}; + +#[derive(Debug, Clone)] +pub struct ProvedPact { + pub pact: Pact, + pub cm_root: [u8; 32], + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedPact { + pub fn prove( + pact: cl::zones::PactWitness, + input_cm_paths: Vec>, + cm_root: [u8; 32], + ) -> Result { + let pact_private = PactPrivate { + pact, + input_cm_paths, + cm_root, + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&pact_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::PACT_ELF, &opts) + .map_err(|_| Error::Risc0ProofFailed)?; + + println!( + "STARK 'pact' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + + Ok(Self { + pact: pact_private.pact.commit(), + cm_root, + risc0_receipt: prove_info.receipt, + }) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(proved_ptx_inputs) = self.public() else { + return false; + }; + let expected_ptx_inputs = PactPublic { + pact: self.pact.clone(), + cm_root: self.cm_root, + }; + if expected_ptx_inputs != proved_ptx_inputs { + return false; + } + + self.risc0_receipt + .verify(nomos_cl_risc0_proofs::PACT_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/src/partial_tx.rs b/emmarin/cl/ledger/src/partial_tx.rs new file mode 100644 index 0000000..0820891 --- /dev/null +++ b/emmarin/cl/ledger/src/partial_tx.rs @@ -0,0 +1,77 @@ +use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; + +use crate::error::{Error, Result}; +use cl::zones::*; + +pub struct ProvedPartialTx { + pub ptx: cl::PartialTx, + pub cm_root: [u8; 32], + pub risc0_receipt: risc0_zkvm::Receipt, +} + +impl ProvedPartialTx { + pub fn prove( + ptx: &cl::PartialTxWitness, + input_cm_paths: Vec>, + cm_root: [u8; 32], + id: ZoneId, + ) -> Result { + let ptx_private = PtxPrivate { + ptx: ptx.clone(), + input_cm_paths, + cm_root, + from: id, + }; + + 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 { + ptx: ptx.commit(&id), + cm_root, + risc0_receipt: prove_info.receipt, + }) + } + + pub fn public(&self) -> Result { + Ok(self.risc0_receipt.journal.decode()?) + } + + pub fn verify(&self) -> bool { + let Ok(proved_ptx_inputs) = self.public() else { + return false; + }; + let expected_ptx_inputs = PtxPublic { + ptx: self.ptx.clone(), + cm_root: self.cm_root, + }; + if expected_ptx_inputs != proved_ptx_inputs { + return false; + } + + self.risc0_receipt + .verify(nomos_cl_risc0_proofs::PTX_ID) + .is_ok() + } +} diff --git a/emmarin/cl/ledger/tests/simple_transfer.rs b/emmarin/cl/ledger/tests/simple_transfer.rs new file mode 100644 index 0000000..879ca19 --- /dev/null +++ b/emmarin/cl/ledger/tests/simple_transfer.rs @@ -0,0 +1,126 @@ +use cl::{note::derive_unit, BalanceWitness}; +use ledger::{ + constraint::ConstraintProof, + ledger::{ProvedLedgerTransition, ProvedZoneTx}, + pact::ProvedPact, +}; +use rand_core::CryptoRngCore; + +struct User(cl::NullifierSecret); + +impl User { + fn random(mut rng: impl CryptoRngCore) -> Self { + Self(cl::NullifierSecret::random(&mut rng)) + } + + fn pk(&self) -> cl::NullifierCommitment { + self.0.commit() + } + + fn sk(&self) -> cl::NullifierSecret { + self.0 + } +} + +fn receive_utxo(note: cl::NoteWitness, nf_pk: cl::NullifierCommitment) -> cl::OutputWitness { + cl::OutputWitness::new(note, nf_pk) +} + +#[test] +fn ledger_transition() { + let nmo = derive_unit("NMO"); + + let mut rng = rand::thread_rng(); + + // 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( + cl::NoteWitness::stateless(10, nmo, ConstraintProof::nop_constraint(), &mut rng), + alice.pk(), + ); + + // Alice wants to send 8 NMO to bob + let bobs_output = cl::OutputWitness::new(cl::NoteWitness::basic(8, nmo, &mut rng), bob.pk()); + + let alice_change = cl::OutputWitness::new(cl::NoteWitness::basic(2, nmo, &mut rng), alice.pk()); + + let alice_input = cl::InputWitness::from_output(utxo, alice.sk()); + + let zone_a = [0; 32]; + let zone_b = [1; 32]; + let ledger_a = cl::zones::LedgerWitness { + commitments: vec![utxo.commit_note(&zone_a)], + nullifiers: vec![], + }; + + let ledger_b = cl::zones::LedgerWitness { + commitments: vec![], + nullifiers: vec![], + }; + + let expected_ledger_a = cl::zones::LedgerWitness { + commitments: vec![utxo.commit_note(&zone_a), alice_change.commit_note(&zone_a)], + nullifiers: vec![alice_input.nullifier(&zone_a)], + }; + + let expected_ledger_b = cl::zones::LedgerWitness { + commitments: vec![bobs_output.commit_note(&zone_b)], + nullifiers: vec![], + }; + + // Construct the ptx consuming Alices inputs and producing the two outputs. + let alice_pact_witness = cl::zones::PactWitness { + tx: cl::PartialTxWitness { + inputs: vec![alice_input], + outputs: vec![bobs_output, alice_change], + + balance_blinding: BalanceWitness::random_blinding(&mut rng), + }, + from: zone_a, + to: vec![zone_b, zone_a], + }; + + let proved_pact = ProvedPact::prove( + alice_pact_witness, + vec![ledger_a + .cm_path(&alice_input.note_commitment(&zone_a)) + .unwrap()], + ledger_a.cm_root(), + ) + .unwrap(); + + assert_eq!(proved_pact.cm_root, ledger_a.cm_root()); + + // Prove the constraints for alices input (she uses the no-op constraint) + let constraint_proof = + ConstraintProof::prove_nop(alice_input.nullifier(&zone_a), proved_pact.pact.tx.root()); + + let ledger_a_transition = ProvedLedgerTransition::prove( + ledger_a, + zone_a, + vec![ProvedZoneTx::Pact(proved_pact.clone())], + vec![constraint_proof], + ) + .unwrap(); + + let ledger_b_transition = ProvedLedgerTransition::prove( + ledger_b, + zone_b, + vec![ProvedZoneTx::Pact(proved_pact)], + vec![], + ) + .unwrap(); + + assert_eq!( + ledger_a_transition.public.ledger, + expected_ledger_a.commit() + ); + assert_eq!( + ledger_b_transition.public.ledger, + expected_ledger_b.commit() + ); +} 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/bundle.rs b/emmarin/cl/ledger_proof_statements/src/bundle.rs new file mode 100644 index 0000000..f45feb9 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/bundle.rs @@ -0,0 +1,12 @@ +use cl::{Balance, BalanceWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePublic { + pub balances: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BundlePrivate { + 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..91e7aa4 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/constraint.rs @@ -0,0 +1,8 @@ +use 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..2d5e1c6 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/ledger.rs @@ -0,0 +1,30 @@ +use crate::bundle::BundlePublic; +use crate::pact::PactPublic; +use crate::ptx::PtxPublic; +use cl::zones::*; +use cl::Output; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerProofPublic { + pub ledger: Ledger, + pub id: ZoneId, + pub cross_in: Vec, + pub cross_out: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LedgerProofPrivate { + pub ledger: LedgerWitness, + pub id: ZoneId, + pub txs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ZoneTx { + LocalTx { + ptxs: Vec, + bundle: BundlePublic, + }, + Pact(PactPublic), +} 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..12c64c5 --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/lib.rs @@ -0,0 +1,5 @@ +pub mod bundle; +pub mod constraint; +pub mod ledger; +pub mod pact; +pub mod ptx; diff --git a/emmarin/cl/ledger_proof_statements/src/pact.rs b/emmarin/cl/ledger_proof_statements/src/pact.rs new file mode 100644 index 0000000..1229c7f --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/pact.rs @@ -0,0 +1,15 @@ +use cl::zones::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PactPublic { + pub pact: Pact, + pub cm_root: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PactPrivate { + pub pact: PactWitness, + pub input_cm_paths: Vec>, + pub cm_root: [u8; 32], +} 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..731408b --- /dev/null +++ b/emmarin/cl/ledger_proof_statements/src/ptx.rs @@ -0,0 +1,16 @@ +use cl::{PartialTx, PartialTxWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PtxPublic { + pub ptx: PartialTx, + pub cm_root: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PtxPrivate { + pub ptx: PartialTxWitness, + pub input_cm_paths: Vec>, + pub cm_root: [u8; 32], + pub from: [u8; 32], +} 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..06cedcb --- /dev/null +++ b/emmarin/cl/ledger_validity_proof/ledger/src/main.rs @@ -0,0 +1,146 @@ +use cl::{zones::*, Output}; +use ledger_proof_statements::{bundle::*, constraint::*, ledger::*, pact::PactPublic, ptx::*}; +use risc0_zkvm::{guest::env, serde}; + +fn main() { + let LedgerProofPrivate { + mut ledger, + id, + txs, + } = env::read(); + + let cm_root = ledger.cm_root(); + + let mut cross_in = vec![]; + let mut cross_out = vec![]; + + for tx in txs { + match tx { + ZoneTx::LocalTx { bundle, ptxs } => { + ledger = process_bundle(ledger, ptxs, bundle, cm_root); + } + ZoneTx::Pact(pact) => { + let (new_ledger, consumed_commits, produced_commits) = + process_pact(ledger, pact, id, cm_root); + ledger = new_ledger; + cross_in.extend(consumed_commits); + cross_out.extend(produced_commits); + } + } + } + + env::commit(&LedgerProofPublic { + ledger: ledger.commit(), + id, + cross_in, + cross_out, + }); +} + +fn process_bundle( + mut ledger: LedgerWitness, + ptxs: Vec, + bundle_proof: BundlePublic, + cm_root: [u8; 32], +) -> LedgerWitness { + assert_eq!( + ptxs.iter().map(|ptx| ptx.ptx.balance).collect::>(), + bundle_proof.balances + ); + // verify bundle is balanced + env::verify( + nomos_cl_risc0_proofs::BUNDLE_ID, + &serde::to_vec(&bundle_proof).unwrap(), + ) + .unwrap(); + + for ptx in ptxs { + ledger = process_ptx(ledger, ptx, cm_root); + } + + ledger +} + +fn process_ptx(mut ledger: LedgerWitness, ptx: PtxPublic, cm_root: [u8; 32]) -> LedgerWitness { + env::verify(nomos_cl_risc0_proofs::PTX_ID, &serde::to_vec(&ptx).unwrap()).unwrap(); + assert_eq!(ptx.cm_root, cm_root); + + let ptx = ptx.ptx; + + for input in &ptx.inputs { + 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 { + ledger.commitments.push(output.note_comm); + } + + ledger +} + +fn process_pact( + mut ledger: LedgerWitness, + pact: PactPublic, + id: ZoneId, + cm_root: [u8; 32], +) -> (LedgerWitness, Vec, Vec) { + let mut cross_in = vec![]; + let mut cross_out = vec![]; + + env::verify( + nomos_cl_risc0_proofs::PACT_ID, + &serde::to_vec(&pact).unwrap(), + ) + .unwrap(); + + let pact_cm_root = pact.cm_root; + let pact = pact.pact; + + if cm_root != pact_cm_root { + // zone is the receiver of the transfer + for (comm, zone) in pact.tx.outputs.iter().zip(&pact.to) { + if *zone == id { + cross_in.push(*comm); + ledger.commitments.push(comm.note_comm); + } + } + } else { + // zone is the sender of the transfer + // proof of non-membership + for input in &pact.tx.inputs { + assert!(!ledger.nullifiers.contains(&input.nullifier)); + ledger.nullifiers.push(input.nullifier); + + env::verify( + input.constraint.0, + &serde::to_vec(&ConstraintPublic { + ptx_root: pact.tx.root(), + nf: input.nullifier, + }) + .unwrap(), + ) + .unwrap(); + } + + for (output, to) in pact.tx.outputs.iter().zip(&pact.to) { + if *to == id { + ledger.commitments.push(output.note_comm); + } else { + cross_out.push(*output); + } + } + } + + (ledger, cross_in, cross_out) +} 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..b34bd02 --- /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 = ["bundle", "constraint_nop", "ptx", "pact"] + 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/bundle/Cargo.toml b/emmarin/cl/risc0_proofs/bundle/Cargo.toml new file mode 100644 index 0000000..f207737 --- /dev/null +++ b/emmarin/cl/risc0_proofs/bundle/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bundle" +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/bundle/src/main.rs b/emmarin/cl/risc0_proofs/bundle/src/main.rs new file mode 100644 index 0000000..9beec67 --- /dev/null +++ b/emmarin/cl/risc0_proofs/bundle/src/main.rs @@ -0,0 +1,23 @@ +/// 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 bundle_private: ledger_proof_statements::bundle::BundlePrivate = env::read(); + + let bundle_public = ledger_proof_statements::bundle::BundlePublic { + balances: Vec::from_iter(bundle_private.balances.iter().map(|b| b.commit())), + }; + + assert!(cl::BalanceWitness::combine(bundle_private.balances, [0u8; 16]).is_zero()); + + env::commit(&bundle_public); +} 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/pact/Cargo.toml b/emmarin/cl/risc0_proofs/pact/Cargo.toml new file mode 100644 index 0000000..9fdfdc0 --- /dev/null +++ b/emmarin/cl/risc0_proofs/pact/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pact" +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/pact/src/main.rs b/emmarin/cl/risc0_proofs/pact/src/main.rs new file mode 100644 index 0000000..04928f0 --- /dev/null +++ b/emmarin/cl/risc0_proofs/pact/src/main.rs @@ -0,0 +1,30 @@ +/// Input Proof +use cl::merkle; +use ledger_proof_statements::pact::{PactPrivate, PactPublic}; +use risc0_zkvm::guest::env; + +fn main() { + let PactPrivate { + pact, + input_cm_paths, + cm_root, + } = env::read(); + + assert_eq!(pact.tx.inputs.len(), input_cm_paths.len()); + for (input, cm_path) in pact.tx.inputs.iter().zip(input_cm_paths) { + let note_cm = input.note_commitment(&pact.from); + let cm_leaf = merkle::leaf(note_cm.as_bytes()); + assert_eq!(cm_root, merkle::path_root(cm_leaf, &cm_path)); + } + + for output in pact.tx.outputs.iter() { + assert!(output.note.value > 0); + } + + assert!(pact.tx.balance().is_zero()); + + env::commit(&PactPublic { + pact: pact.commit(), + cm_root, + }); +} 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..9af2f5d --- /dev/null +++ b/emmarin/cl/risc0_proofs/ptx/src/main.rs @@ -0,0 +1,29 @@ +/// Input Proof +use cl::merkle; +use ledger_proof_statements::ptx::{PtxPrivate, PtxPublic}; +use risc0_zkvm::guest::env; + +fn main() { + let PtxPrivate { + ptx, + input_cm_paths, + cm_root, + from, + } = env::read(); + + assert_eq!(ptx.inputs.len(), input_cm_paths.len()); + for (input, cm_path) in ptx.inputs.iter().zip(input_cm_paths) { + let note_cm = input.note_commitment(&from); + 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(&from), + cm_root, + }); +} 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"));