This commit is contained in:
Giacomo Pasini 2024-11-18 14:07:04 +01:00
parent 893ab94ce0
commit ec5cd13d46
No known key found for this signature in database
GPG Key ID: FC08489D2D895D4B
48 changed files with 2418 additions and 0 deletions

2
emmarin/cl/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock
target/

11
emmarin/cl/Cargo.toml Normal file
View File

@ -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

15
emmarin/cl/cl/Cargo.toml Normal file
View File

@ -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"

View File

@ -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<UnitBalance>,
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<Item = Self>, 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)
}
}

117
emmarin/cl/cl/src/bundle.rs Normal file
View File

@ -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<PartialTx>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleWitness {
pub partials: Vec<PartialTxWitness>,
}
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![]);
}
}

View File

@ -0,0 +1,6 @@
use curve25519_dalek::ristretto::RistrettoPoint;
use sha2::Sha512;
pub fn hash_to_curve(bytes: &[u8]) -> RistrettoPoint {
RistrettoPoint::hash_from_bytes::<Sha512>(bytes)
}

View File

@ -0,0 +1,4 @@
#[derive(Debug)]
pub enum Error {
ProofFailed,
}

View File

@ -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
}
}

21
emmarin/cl/cl/src/lib.rs Normal file
View File

@ -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,
};

239
emmarin/cl/cl/src/merkle.rs Normal file
View File

@ -0,0 +1,239 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub fn padded_leaves<const N: usize>(elements: &[Vec<u8>]) -> [[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<const N: usize>(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<const N: usize>(leaves: [[u8; 32]; N], idx: usize) -> Vec<PathNode> {
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);
}
}

171
emmarin/cl/cl/src/note.rs Normal file
View File

@ -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)
// );
// }
}

View File

@ -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);
// }
}

View File

@ -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
}
}

View File

@ -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<Input>,
pub outputs: Vec<Output>,
pub balance: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartialTxWitness {
pub inputs: Vec<InputWitness>,
pub outputs: Vec<OutputWitness>,
pub balance_blinding: [u8; 16],
}
impl PartialTxWitness {
pub fn random(
inputs: Vec<InputWitness>,
outputs: Vec<OutputWitness>,
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::<MAX_INPUTS>(&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::<MAX_OUTPUTS>(&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::<MAX_INPUTS>(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::<MAX_OUTPUTS>(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<merkle::PathNode>,
}
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<merkle::PathNode>,
}
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()
// );
// }
}

100
emmarin/cl/cl/src/zones.rs Normal file
View File

@ -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<ZoneId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PactWitness {
pub tx: PartialTxWitness,
pub from: ZoneId,
pub to: Vec<ZoneId>,
}
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<NoteCommitment>,
pub nullifiers: Vec<Nullifier>,
}
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::<Vec<_>>();
merkle::root(merkle::padded_leaves::<MAX_NULL>(&bytes))
}
pub fn cm_root(&self) -> [u8; 32] {
let bytes = self
.commitments
.iter()
.map(|i| i.as_bytes().to_vec())
.collect::<Vec<_>>();
merkle::root(merkle::padded_leaves::<MAX_COMM>(&bytes))
}
pub fn cm_path(&self, cm: &NoteCommitment) -> Option<Vec<merkle::PathNode>> {
let bytes = self
.commitments
.iter()
.map(|i| i.as_bytes().to_vec())
.collect::<Vec<_>>();
let leaves = merkle::padded_leaves::<MAX_COMM>(&bytes);
let idx = self.commitments.iter().position(|c| c == cm)?;
Some(merkle::path(leaves, idx))
}
}

View File

@ -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())
}

View File

@ -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"

View File

@ -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<Self> {
// 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<ledger_proof_statements::bundle::BundlePublic> {
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()
}
}

View File

@ -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<ConstraintPublic> {
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)
}
}

View File

@ -0,0 +1,11 @@
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Error>;
#[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,
}

View File

@ -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<ProvedPartialTx>,
},
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<risc0_zkvm::Receipt> {
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<ProvedZoneTx>,
constraints: Vec<ConstraintProof>,
) -> Result<Self> {
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::<LedgerProofPublic>()
.unwrap(),
risc0_receipt: prove_info.receipt,
})
}
pub fn public(&self) -> Result<LedgerProofPublic> {
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()
}
}

View File

@ -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;

View File

@ -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<Vec<cl::merkle::PathNode>>,
cm_root: [u8; 32],
) -> Result<ProvedPact> {
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<PactPublic> {
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()
}
}

View File

@ -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<Vec<cl::merkle::PathNode>>,
cm_root: [u8; 32],
id: ZoneId,
) -> Result<ProvedPartialTx> {
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<PtxPublic> {
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()
}
}

View File

@ -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()
);
}

View File

@ -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"] }

View File

@ -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<Balance>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundlePrivate {
pub balances: Vec<BalanceWitness>,
}

View File

@ -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,
}

View File

@ -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<Output>,
pub cross_out: Vec<Output>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LedgerProofPrivate {
pub ledger: LedgerWitness,
pub id: ZoneId,
pub txs: Vec<ZoneTx>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ZoneTx {
LocalTx {
ptxs: Vec<PtxPublic>,
bundle: BundlePublic,
},
Pact(PactPublic),
}

View File

@ -0,0 +1,5 @@
pub mod bundle;
pub mod constraint;
pub mod ledger;
pub mod pact;
pub mod ptx;

View File

@ -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<Vec<cl::merkle::PathNode>>,
pub cm_root: [u8; 32],
}

View File

@ -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<Vec<cl::merkle::PathNode>>,
pub cm_root: [u8; 32],
pub from: [u8; 32],
}

View File

@ -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"]

View File

@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -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" }

View File

@ -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<PtxPublic>,
bundle_proof: BundlePublic,
cm_root: [u8; 32],
) -> LedgerWitness {
assert_eq!(
ptxs.iter().map(|ptx| ptx.ptx.balance).collect::<Vec<_>>(),
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<Output>, Vec<Output>) {
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)
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@ -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"]

View File

@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -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" }

View File

@ -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);
}

View File

@ -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" }

View File

@ -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);
}

View File

@ -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" }

View File

@ -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,
});
}

View File

@ -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" }

View File

@ -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,
});
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));