diff --git a/goas/atomic_asset_transfer/common/Cargo.toml b/goas/atomic_asset_transfer/common/Cargo.toml index 8c428bf..1308a01 100644 --- a/goas/atomic_asset_transfer/common/Cargo.toml +++ b/goas/atomic_asset_transfer/common/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } cl = { path = "../../cl/cl" } -goas_proof_statements = { path = "../proof_statements" } -proof_statements = { path = "../../cl/proof_statements" } +ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } once_cell = "1" -sha2 = "0.10" \ No newline at end of file +sha2 = "0.10" diff --git a/goas/atomic_asset_transfer/common/src/events.rs b/goas/atomic_asset_transfer/common/src/events.rs new file mode 100644 index 0000000..b75a259 --- /dev/null +++ b/goas/atomic_asset_transfer/common/src/events.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Event { + Spend(Spend), +} + +impl Event { + pub fn to_bytes(&self) -> Vec { + // TODO: add variant tag to byte encoding + match self { + Event::Spend(spend) => spend.to_bytes().to_vec(), + } + } +} + +/// An event that authorizes spending zone funds +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Spend { + pub amount: u64, + /// The public key of the recipient + pub to: cl::NullifierCommitment, + /// The nullifier of note that is being spent, this is to avoid using the spend event to + /// for multiple notes + pub fund_nf: cl::Nullifier, +} + +impl Spend { + pub fn to_bytes(&self) -> [u8; 72] { + let mut bytes = [0; 72]; + bytes[0..8].copy_from_slice(&self.amount.to_le_bytes()); + bytes[8..40].copy_from_slice(self.to.as_bytes()); + bytes[40..72].copy_from_slice(self.fund_nf.as_bytes()); + bytes + } +} diff --git a/goas/atomic_asset_transfer/common/src/lib.rs b/goas/atomic_asset_transfer/common/src/lib.rs index dbc18c5..5e59831 100644 --- a/goas/atomic_asset_transfer/common/src/lib.rs +++ b/goas/atomic_asset_transfer/common/src/lib.rs @@ -1,6 +1,7 @@ +pub mod events; + use cl::{ balance::Unit, - crypto, input::InputWitness, nullifier::{Nullifier, NullifierCommitment}, output::OutputWitness, @@ -17,14 +18,14 @@ pub const MAX_EVENTS: usize = 1 << 8; // state of the zone #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct StateCommitment([u8; 32]); +pub struct StateCommitment(pub [u8; 32]); pub type AccountId = u32; // PLACEHOLDER: this is probably going to be NMO? -pub static ZONE_CL_FUNDS_UNIT: Lazy = Lazy::new(|| crypto::hash_to_curve(b"NMO")); +pub static ZONE_CL_FUNDS_UNIT: Lazy = Lazy::new(|| cl::note::unit_point("NMO")); -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ZoneMetadata { pub zone_vk: [u8; 32], pub funds_vk: [u8; 32], @@ -41,11 +42,11 @@ impl ZoneMetadata { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StateWitness { - pub balances: BTreeMap, + pub balances: BTreeMap, pub included_txs: Vec, - pub output_events: Vec, + pub output_events: Vec, pub zone_metadata: ZoneMetadata, } @@ -67,34 +68,61 @@ impl StateWitness { StateCommitment(root) } - fn events_root(&self) -> [u8; 32] { - let event_bytes = Vec::from_iter( - self.output_events - .iter() - .map(Event::to_bytes) - .map(Vec::from_iter), - ); + pub fn withdraw(mut self, w: Withdraw) -> Self { + self.included_txs.push(Input::Withdraw(w)); + + let Withdraw { + from, + amount, + to, + fund_nf, + } = w; + + let from_balance = self.balances.entry(from).or_insert(0); + *from_balance = from_balance + .checked_sub(amount) + .expect("insufficient funds in account"); + + let spend_auth = events::Spend { + amount, + to, + fund_nf, + }; + + self.output_events.push(events::Event::Spend(spend_auth)); + self + } + + pub fn events_root(&self) -> [u8; 32] { + let event_bytes = Vec::from_iter(self.output_events.iter().map(events::Event::to_bytes)); let event_merkle_leaves = cl::merkle::padded_leaves(&event_bytes); cl::merkle::root::(event_merkle_leaves) } - fn included_txs_root(&self) -> [u8; 32] { + pub fn included_txs_root(&self) -> [u8; 32] { // this is a placeholder let tx_bytes = [vec![0u8; 32]]; let tx_merkle_leaves = cl::merkle::padded_leaves(&tx_bytes); cl::merkle::root::(tx_merkle_leaves) } - fn balances_root(&self) -> [u8; 32] { - let balance_bytes = Vec::from_iter(self.balances.iter().map(|(k, v)| { - let mut bytes = [0; 8]; - bytes.copy_from_slice(&k.to_le_bytes()); - bytes[8..].copy_from_slice(&v.to_le_bytes()); - bytes.to_vec() + pub fn balances_root(&self) -> [u8; 32] { + let balance_bytes = Vec::from_iter(self.balances.iter().map(|(owner, balance)| { + let mut bytes: Vec = vec![]; + bytes.extend(owner.to_le_bytes()); + bytes.extend(balance.to_le_bytes()); + bytes })); let balance_merkle_leaves = cl::merkle::padded_leaves(&balance_bytes); cl::merkle::root::(balance_merkle_leaves) } + + pub fn event_merkle_path(&self, event: events::Event) -> Vec { + let idx = self.output_events.iter().position(|e| e == &event).unwrap(); + let event_bytes = Vec::from_iter(self.output_events.iter().map(events::Event::to_bytes)); + let event_merkle_leaves = cl::merkle::padded_leaves(&event_bytes); + cl::merkle::path::(event_merkle_leaves, idx) + } } impl From for [u8; 32] { @@ -103,27 +131,35 @@ impl From for [u8; 32] { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Withdraw { pub from: AccountId, - pub amount: AccountId, + pub amount: u64, pub to: NullifierCommitment, - pub nf: Nullifier, + pub fund_nf: Nullifier, } impl Withdraw { + pub fn to_event(&self) -> events::Spend { + events::Spend { + amount: self.amount, + to: self.to, + fund_nf: self.fund_nf, + } + } + pub fn to_bytes(&self) -> [u8; 72] { let mut bytes = [0; 72]; bytes[0..4].copy_from_slice(&self.from.to_le_bytes()); bytes[4..8].copy_from_slice(&self.amount.to_le_bytes()); bytes[8..40].copy_from_slice(self.to.as_bytes()); - bytes[40..72].copy_from_slice(self.nf.as_bytes()); + bytes[40..72].copy_from_slice(self.fund_nf.as_bytes()); bytes } } /// A deposit of funds into the zone -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Deposit { /// The note that is used to deposit funds into the zone pub deposit: InputWitness, @@ -137,21 +173,8 @@ pub struct Deposit { pub zone_funds_out: OutputWitness, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Input { Withdraw(Withdraw), Deposit(Deposit), } - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum Event { - Spend(goas_proof_statements::zone_funds::Spend), -} - -impl Event { - pub fn to_bytes(&self) -> Vec { - match self { - Event::Spend(spend) => spend.to_bytes().to_vec(), - } - } -} diff --git a/goas/atomic_asset_transfer/executor/Cargo.toml b/goas/atomic_asset_transfer/executor/Cargo.toml index 0441c2f..ce38b7a 100644 --- a/goas/atomic_asset_transfer/executor/Cargo.toml +++ b/goas/atomic_asset_transfer/executor/Cargo.toml @@ -16,4 +16,8 @@ common = { path = "../common" } tempfile = "3" clap = { version = "4", features = ["derive"] } rand = "0.8.5" +rand_core = "0.6.0" cl = { path = "../../cl/cl" } +ledger = { path = "../../cl/ledger" } +ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } +goas_proof_statements = { path = "../proof_statements" } \ No newline at end of file diff --git a/goas/atomic_asset_transfer/executor/src/bin/groth16.rs b/goas/atomic_asset_transfer/executor/src/bin/groth16.rs index c504326..a8fc2ee 100644 --- a/goas/atomic_asset_transfer/executor/src/bin/groth16.rs +++ b/goas/atomic_asset_transfer/executor/src/bin/groth16.rs @@ -4,10 +4,9 @@ /// This workaround manually calls into docker after creating a directory with the required permissions. /// In addition, splitting the process in different stages highlights better the different work that /// needs to be done which could be split across different actors. - -use std::path::PathBuf; use clap::Parser; use risc0_zkvm::{get_prover_server, ProverOpts, Receipt}; +use std::path::PathBuf; const WORK_DIR_ENV: &str = "RISC0_WORK_DIR"; @@ -47,8 +46,14 @@ fn main() -> Result<(), Box> { risc0_groth16::docker::stark_to_snark(&converted.get_seal_bytes())?; std::fs::create_dir_all(&args.output_dir)?; - std::fs::copy(work_dir_path.join("proof.json"), args.output_dir.join("proof.json"))?; - std::fs::copy(work_dir_path.join("public.json"), args.output_dir.join("public.json"))?; - + std::fs::copy( + work_dir_path.join("proof.json"), + args.output_dir.join("proof.json"), + )?; + std::fs::copy( + work_dir_path.join("public.json"), + args.output_dir.join("public.json"), + )?; + Ok(()) - } \ No newline at end of file +} diff --git a/goas/atomic_asset_transfer/executor/src/lib.rs b/goas/atomic_asset_transfer/executor/src/lib.rs new file mode 100644 index 0000000..3b92436 --- /dev/null +++ b/goas/atomic_asset_transfer/executor/src/lib.rs @@ -0,0 +1,73 @@ +use common::{events::Event, Input, StateWitness}; +use goas_proof_statements::{zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate}; + +pub fn prove_zone_stf( + state: StateWitness, + inputs: Vec, + zone_in: cl::PartialTxInputWitness, + zone_out: cl::PartialTxOutputWitness, +) -> ledger::DeathProof { + let private_inputs = ZoneStatePrivate { + state, + inputs, + zone_in, + zone_out, + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&private_inputs) + .unwrap() + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + + use std::time::Instant; + let start_t = Instant::now(); + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, goas_risc0_proofs::ZONE_STATE_ELF, &opts) + .unwrap(); + println!("STARK 'zone_stf' prover time: {:.2?}", start_t.elapsed()); + let receipt = prove_info.receipt; + ledger::DeathProof::from_risc0(goas_risc0_proofs::ZONE_STATE_ID, receipt) +} + +pub fn prove_zone_fund_withdraw( + in_zone_funds: cl::PartialTxInputWitness, + zone_note: cl::PartialTxOutputWitness, + out_zone_funds: cl::PartialTxOutputWitness, + spent_note: cl::PartialTxOutputWitness, + out_zone_state: &StateWitness, + withdraw: common::Withdraw, +) -> ledger::DeathProof { + let spend_event = withdraw.to_event(); + let private_inputs = SpendFundsPrivate { + in_zone_funds, + zone_note, + out_zone_funds, + spent_note, + spend_event, + spend_event_state_path: out_zone_state.event_merkle_path(Event::Spend(spend_event)), + balances_root: out_zone_state.balances_root(), + txs_root: out_zone_state.included_txs_root(), + }; + + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&private_inputs) + .unwrap() + .build() + .unwrap(); + + let prover = risc0_zkvm::default_prover(); + + use std::time::Instant; + let start_t = Instant::now(); + let opts = risc0_zkvm::ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, goas_risc0_proofs::SPEND_ZONE_FUNDS_ELF, &opts) + .unwrap(); + println!("STARK 'zone_fund' prover time: {:.2?}", start_t.elapsed()); + let receipt = prove_info.receipt; + ledger::DeathProof::from_risc0(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, receipt) +} diff --git a/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs new file mode 100644 index 0000000..55c245f --- /dev/null +++ b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs @@ -0,0 +1,157 @@ +use std::collections::BTreeMap; + +use cl::{NoteWitness, NullifierNonce, NullifierSecret}; +use common::{events::Event, Input, StateWitness, ZoneMetadata, ZONE_CL_FUNDS_UNIT}; +use ledger::death_constraint::DeathProof; +use rand_core::CryptoRngCore; + +fn zone_state_death_constraint() -> [u8; 32] { + ledger::death_constraint::risc0_id_to_cl_death_constraint(goas_risc0_proofs::ZONE_STATE_ID) +} + +fn zone_fund_death_constraint() -> [u8; 32] { + ledger::death_constraint::risc0_id_to_cl_death_constraint( + goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, + ) +} + +fn zone_fund_utxo( + value: u64, + zone_meta: ZoneMetadata, + mut rng: impl CryptoRngCore, +) -> cl::OutputWitness { + cl::OutputWitness::public( + cl::NoteWitness { + value, + unit: *common::ZONE_CL_FUNDS_UNIT, + death_constraint: zone_meta.funds_vk, + state: zone_meta.id(), + }, + NullifierNonce::random(&mut rng), + ) +} + +fn zone_state_utxo(zone: &StateWitness, mut rng: impl CryptoRngCore) -> cl::OutputWitness { + cl::OutputWitness::public( + cl::NoteWitness { + value: 1, + unit: zone.zone_metadata.unit, + death_constraint: zone.zone_metadata.zone_vk, + state: zone.commit().0, + }, + NullifierNonce::random(&mut rng), + ) +} + +#[test] +fn test_withdrawal() { + let mut rng = rand::thread_rng(); + + let alice = 42; + let alice_sk = NullifierSecret::random(&mut rng); + + let init_state = StateWitness { + balances: BTreeMap::from_iter([(alice, 100)]), + included_txs: vec![], + output_events: vec![], + zone_metadata: ZoneMetadata { + zone_vk: zone_state_death_constraint(), + funds_vk: zone_fund_death_constraint(), + unit: cl::note::unit_point("ZONE_STATE"), + }, + }; + + let zone_fund_in = + cl::InputWitness::public(zone_fund_utxo(35240, init_state.zone_metadata, &mut rng)); + let zone_state_in = cl::InputWitness::public(zone_state_utxo(&init_state, &mut rng)); + + let withdraw = common::Withdraw { + from: alice, + amount: 78, + to: alice_sk.commit(), + fund_nf: zone_fund_in.nullifier(), + }; + + let end_state = init_state.clone().withdraw(withdraw); + + let zone_state_out = cl::OutputWitness::public( + cl::NoteWitness { + state: end_state.commit().0, + ..zone_state_in.note + }, + zone_state_in.evolved_nonce(), + ); + let zone_fund_out = cl::OutputWitness::public( + cl::NoteWitness { + value: zone_fund_in.note.value - withdraw.amount, + ..zone_fund_in.note + }, + zone_fund_in.evolved_nonce(), + ); + + let alice_withdrawal = cl::OutputWitness::random( + NoteWitness::stateless( + withdraw.amount, + *ZONE_CL_FUNDS_UNIT, + DeathProof::nop_constraint(), + ), + alice_sk.commit(), + &mut rng, + ); + + let withdraw_ptx = cl::PartialTxWitness { + inputs: vec![zone_state_in, zone_fund_in], + outputs: vec![zone_state_out, zone_fund_out, alice_withdrawal], + }; + + let death_proofs = BTreeMap::from_iter([ + ( + zone_state_in.nullifier(), + executor::prove_zone_stf( + init_state.clone(), + vec![Input::Withdraw(withdraw)], + withdraw_ptx.input_witness(0), // input state note (input #0) + withdraw_ptx.output_witness(0), // output state note (output #0) + ), + ), + ( + zone_fund_in.nullifier(), + executor::prove_zone_fund_withdraw( + withdraw_ptx.input_witness(1), // input fund note (input #1) + withdraw_ptx.output_witness(0), // output state note (output #0) + withdraw_ptx.output_witness(1), // output state note (output #0) + withdraw_ptx.output_witness(2), // output state note (output #0) + &end_state, + withdraw, + ), + ), + ]); + + let note_commitments = vec![ + zone_state_in.note_commitment(), + zone_fund_in.note_commitment(), + ]; + + let withdraw_proof = + ledger::partial_tx::ProvedPartialTx::prove(&withdraw_ptx, death_proofs, ¬e_commitments) + .expect("withdraw proof failed"); + + assert!(withdraw_proof.verify()); + + assert_eq!(withdraw_proof.outputs[0].output, zone_state_out.commit()); + assert_eq!( + zone_state_out.note.state, + StateWitness { + balances: BTreeMap::from_iter([(alice, 22)]), + included_txs: vec![Input::Withdraw(withdraw)], + output_events: vec![Event::Spend(common::events::Spend { + amount: 78, + to: alice_sk.commit(), + fund_nf: zone_fund_in.nullifier() + })], + zone_metadata: init_state.zone_metadata + } + .commit() + .0 + ) +} diff --git a/goas/atomic_asset_transfer/proof_statements/Cargo.toml b/goas/atomic_asset_transfer/proof_statements/Cargo.toml index d7b9b87..2de8d41 100644 --- a/goas/atomic_asset_transfer/proof_statements/Cargo.toml +++ b/goas/atomic_asset_transfer/proof_statements/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +common = { path = "../common" } cl = { path = "../../cl/cl" } -proof_statements = { path = "../../cl/proof_statements" } serde = { version = "1.0", features = ["derive"] } diff --git a/goas/atomic_asset_transfer/proof_statements/src/lib.rs b/goas/atomic_asset_transfer/proof_statements/src/lib.rs index 3eba170..08a947a 100644 --- a/goas/atomic_asset_transfer/proof_statements/src/lib.rs +++ b/goas/atomic_asset_transfer/proof_statements/src/lib.rs @@ -1 +1,2 @@ pub mod zone_funds; +pub mod zone_state; diff --git a/goas/atomic_asset_transfer/proof_statements/src/user_note.rs b/goas/atomic_asset_transfer/proof_statements/src/user_note.rs new file mode 100644 index 0000000..61a9501 --- /dev/null +++ b/goas/atomic_asset_transfer/proof_statements/src/user_note.rs @@ -0,0 +1,24 @@ +/// The User Note encodes the logic of the atomic asset transfer +/// +/// The scenario is as follows: +/// The user, let's call her Alice has 100 NMO in Zone A and she wants to move it to +/// Zone B. She wants to arrange this transfer so that both the withdrawal from Zone +/// A and the deposit to Zone B occur atomically. +/// +/// The Alice will create a partial tx that looks like this: +/// +/// [fee note] -> [user note] +/// +/// The User Note will encode the logic that orchestrates the withdrawal from zone A +/// and deposit to zone B. +/// +/// The User Notes death constraint requires the following statements to be satisfied +/// in order for the fee to be captured. +/// +/// 1. w_tx = withdraw(amt=100 NMO, from=Alice) tx was included in Zone A. +/// 2. d_tx = deposit(amt=100 NMO, to=Alice) tx was included in Zone B. +/// 3. w_tx is included in Zone A iff d_tx is included in Zone B +/// +/// Details: +/// - the withdrawal in zone A must not be a general withdrawal tx, it must be bound to the user note. +/// i.e. the user_note must be present in the ptx for the withdrawal to be valid in Zone A. diff --git a/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs b/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs index 0343902..1a77d10 100644 --- a/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs +++ b/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs @@ -1,39 +1,17 @@ -use proof_statements::{ptx::PartialTxInputPrivate, ptx::PartialTxOutputPrivate}; use serde::{Deserialize, Serialize}; -/// An event that authorizes spending zone funds -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct Spend { - pub amount: u64, - /// The public key of the recipient - pub to: cl::NullifierCommitment, - /// The nullifier of note that is being spent, this is to avoid using the spend event to - /// for multiple notes - pub nf: cl::Nullifier, -} - -impl Spend { - pub fn to_bytes(&self) -> [u8; 72] { - let mut bytes = [0; 72]; - bytes[0..8].copy_from_slice(&self.amount.to_le_bytes()); - bytes[8..40].copy_from_slice(self.to.as_bytes()); - bytes[40..72].copy_from_slice(self.nf.as_bytes()); - bytes - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SpendFundsPrivate { /// The note we're spending - pub in_zone_funds: PartialTxInputPrivate, + pub in_zone_funds: cl::PartialTxInputWitness, /// The zone note that is authorizing the spend - pub zone_note: PartialTxOutputPrivate, + pub zone_note: cl::PartialTxOutputWitness, /// The note that is being created to send the change back to the zone - pub out_zone_funds: PartialTxOutputPrivate, + pub out_zone_funds: cl::PartialTxOutputWitness, /// The spent funds note - pub spent_note: PartialTxOutputPrivate, + pub spent_note: cl::PartialTxOutputWitness, /// The event emitted by the zone that authorizes the spend - pub spend_event: Spend, + pub spend_event: common::events::Spend, /// Path to the zone output events root pub spend_event_state_path: Vec, /// Merkle root of txs included in the zone diff --git a/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs b/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs new file mode 100644 index 0000000..efe8511 --- /dev/null +++ b/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs @@ -0,0 +1,10 @@ +use common::{Input, StateWitness}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ZoneStatePrivate { + pub state: StateWitness, + pub inputs: Vec, + pub zone_in: cl::PartialTxInputWitness, + pub zone_out: cl::PartialTxOutputWitness, +} diff --git a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml index 4d65001..1d349a5 100644 --- a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml +++ b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/Cargo.toml @@ -10,7 +10,7 @@ risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../../cl/cl" } goas_proof_statements = { path = "../../proof_statements" } -proof_statements = { path = "../../../cl/proof_statements" } +ledger_proof_statements = { path = "../../../cl/ledger_proof_statements" } sha2 = "0.10" diff --git a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs index d4b59de..b8cac71 100644 --- a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs +++ b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs @@ -2,9 +2,9 @@ /// /// Our goal: prove the zone authorized spending of funds use cl::merkle; -use cl::nullifier::{Nullifier, NullifierSecret}; +use cl::partial_tx::PtxRoot; use goas_proof_statements::zone_funds::SpendFundsPrivate; -use proof_statements::death_constraint::DeathConstraintPublic; +use ledger_proof_statements::death_constraint::DeathConstraintPublic; use risc0_zkvm::guest::env; fn main() { @@ -19,11 +19,12 @@ fn main() { balances_root, } = env::read(); - let ptx_root = in_zone_funds.ptx_root(); - let nf = Nullifier::new(in_zone_funds.input.nf_sk, in_zone_funds.input.nonce); - // check the zone funds note is the one in the spend event - assert_eq!(nf, spend_event.nf); - assert_eq!(ptx_root, zone_note.ptx_root()); + let input_root = in_zone_funds.input_root(); + let output_root = out_zone_funds.output_root(); + + assert_eq!(output_root, zone_note.output_root()); + assert_eq!(output_root, spent_note.output_root()); + assert_eq!(output_root, out_zone_funds.output_root()); // ** Assert the spent event was an output of the correct zone stf ** // The zone state field is a merkle tree over: @@ -45,8 +46,6 @@ fn main() { zone_note.output.note.state ); - assert_eq!(ptx_root, out_zone_funds.ptx_root()); - // Check we return the rest of the funds back to the zone let change = in_zone_funds .input @@ -57,39 +56,41 @@ fn main() { assert_eq!(out_zone_funds.output.note.value, change); // zone funds output should have the same death constraints as the zone funds input assert_eq!( - out_zone_funds.output.note.death_constraint, - in_zone_funds.input.note.death_constraint + in_zone_funds.input.note.death_constraint, + out_zone_funds.output.note.death_constraint ); assert_eq!( - out_zone_funds.output.note.unit, - in_zone_funds.input.note.unit + in_zone_funds.input.note.unit, + out_zone_funds.output.note.unit ); - // zone funds nullifier, nonce and value blinding should be public so that everybody can spend it + // ensure zone fund sk's, blindings and nonces are propagated correctly. assert_eq!( - out_zone_funds.output.nf_pk, - NullifierSecret::from_bytes([0; 16]).commit() + in_zone_funds.input.nf_sk.commit(), + out_zone_funds.output.nf_pk ); assert_eq!( - out_zone_funds.output.balance_blinding, - in_zone_funds.input.balance_blinding + in_zone_funds.input.balance_blinding, + out_zone_funds.output.balance_blinding ); assert_eq!( + in_zone_funds.input.evolved_nonce(), out_zone_funds.output.nonce, - in_zone_funds.input.evolved_nonce() ); // the state is propagated assert_eq!( + in_zone_funds.input.note.state, out_zone_funds.output.note.state, - in_zone_funds.input.note.state ); - assert_eq!(ptx_root, spent_note.ptx_root()); - // check the correct amount of funds is being spent assert_eq!(spent_note.output.note.value, spend_event.amount); assert_eq!(spent_note.output.note.unit, in_zone_funds.input.note.unit); // check the correct recipient is being paid assert_eq!(spent_note.output.nf_pk, spend_event.to); + let nf = in_zone_funds.input.nullifier(); + assert_eq!(nf, spend_event.fund_nf); // ensure this event was meant for this note. + + let ptx_root = PtxRoot(merkle::node(input_root, output_root)); env::commit(&DeathConstraintPublic { ptx_root, nf }); } diff --git a/goas/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml b/goas/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml index ec3f512..7f10de5 100644 --- a/goas/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml +++ b/goas/atomic_asset_transfer/risc0_proofs/zone_state/Cargo.toml @@ -15,7 +15,7 @@ bincode = "1" common = { path = "../../common" } cl = { path = "../../../cl/cl" } goas_proof_statements = { path = "../../proof_statements" } -proof_statements = { path = "../../../cl/proof_statements" } +ledger_proof_statements = { path = "../../../cl/ledger_proof_statements" } sha2 = "0.10" [patch.crates-io] diff --git a/goas/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs b/goas/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs index 50216c6..db36372 100644 --- a/goas/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs +++ b/goas/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs @@ -1,5 +1,4 @@ use cl::{ - input::InputWitness, merkle, nullifier::{Nullifier, NullifierSecret}, partial_tx::{MAX_INPUTS, MAX_OUTPUTS}, @@ -7,37 +6,10 @@ use cl::{ }; use common::*; -use goas_proof_statements::zone_funds::Spend; -use proof_statements::{ - death_constraint::DeathConstraintPublic, - ptx::{PartialTxInputPrivate, PartialTxOutputPrivate}, -}; +use goas_proof_statements::zone_state::ZoneStatePrivate; +use ledger_proof_statements::death_constraint::DeathConstraintPublic; use risc0_zkvm::guest::env; -fn withdraw(mut state: StateWitness, withdraw: Withdraw) -> StateWitness { - state.included_txs.push(Input::Withdraw(withdraw)); - - let Withdraw { - from, - amount, - to, - nf, - } = withdraw; - - let from_balance = state.balances.entry(from).or_insert(0); - *from_balance = from - .checked_sub(amount) - .expect("insufficient funds in account"); - let spend_auth = Spend { - amount: amount.into(), - to, - nf, - }; - - state.output_events.push(Event::Spend(spend_auth)); - state -} - fn deposit( mut state: StateWitness, deposit: Deposit, @@ -114,7 +86,7 @@ fn deposit( assert_eq!(nullifier, pub_inputs.nf); // 6) We're now ready to do the deposit! - let amount = deposit.note.value as u32; + let amount = deposit.note.value; let to = AccountId::from_be_bytes(<[u8; 4]>::try_from(&deposit.note.state[0..4]).unwrap()); let to_balance = state.balances.entry(to).or_insert(0); @@ -125,59 +97,68 @@ fn deposit( state } -fn validate_zone_input( - input: &PartialTxInputPrivate, - state: &StateWitness, -) -> (PtxRoot, Nullifier) { - let ptx_root = input.ptx_root(); - let nf = Nullifier::new(input.input.nf_sk, input.input.nonce); +fn validate_zone_transition( + in_note: cl::PartialTxInputWitness, + out_note: cl::PartialTxOutputWitness, + in_meta: ZoneMetadata, + in_state_cm: StateCommitment, + out_state: StateWitness, +) { + // Ensure input/output notes are committing to the expected states. + assert_eq!(in_note.input.note.state, in_state_cm.0); + assert_eq!(out_note.output.note.state, out_state.commit().0); - assert_eq!(input.input.note.state, <[u8; 32]>::from(state.commit())); - // should not be possible to create one but let's put this check here just in case - debug_assert_eq!( - input.input.note.death_constraint, - state.zone_metadata.zone_vk + // zone metadata is propagated + assert_eq!(out_state.zone_metadata.id(), in_meta.id()); + + // ensure units match metadata + assert_eq!(in_note.input.note.unit, in_meta.unit); + assert_eq!(out_note.output.note.unit, in_meta.unit); + + // ensure constraints match metadata + assert_eq!(in_note.input.note.death_constraint, in_meta.zone_vk); + assert_eq!(out_note.output.note.death_constraint, in_meta.zone_vk); + + // nullifier secret is propagated + assert_eq!(in_note.input.nf_sk.commit(), out_note.output.nf_pk); + + // balance blinding is propagated + assert_eq!( + in_note.input.balance_blinding, + out_note.output.balance_blinding ); - (ptx_root, nf) -} - -fn validate_zone_output( - ptx: PtxRoot, - input: InputWitness, - output: PartialTxOutputPrivate, - state: &StateWitness, -) { - assert_eq!(ptx, output.ptx_root()); // the ptx root is the same as in the input - let output = output.output; - assert_eq!(output.note.state, <[u8; 32]>::from(state.commit())); // the state in the output is as calculated by this function - assert_eq!(output.note.death_constraint, state.zone_metadata.zone_vk); // the death constraint is the correct one - assert_eq!(output.nf_pk, NullifierSecret::from_bytes([0; 16]).commit()); // the nullifier secret is public - assert_eq!(output.balance_blinding, input.balance_blinding); // the balance blinding is the same as in the input - assert_eq!(output.note.unit, state.zone_metadata.unit); // the balance unit is the same as in the input - // the nonce is correctly evolved - assert_eq!(output.nonce, input.evolved_nonce()); + assert_eq!(in_note.input.evolved_nonce(), out_note.output.nonce); } fn main() { - let zone_in: PartialTxInputPrivate = env::read(); - let mut state: StateWitness = env::read(); - let zone_out: PartialTxOutputPrivate = env::read(); + let ZoneStatePrivate { + mut state, + inputs, + zone_in, + zone_out, + } = env::read(); - let (ptx_root, nf) = validate_zone_input(&zone_in, &state); + let pub_inputs = DeathConstraintPublic { + ptx_root: PtxRoot(cl::merkle::node( + zone_in.input_root(), + zone_out.output_root(), + )), + nf: zone_in.input.nullifier(), + }; - let pub_inputs = DeathConstraintPublic { ptx_root, nf }; - - let inputs: Vec = env::read(); + let in_meta = state.zone_metadata; + let in_state_cm = state.commit(); for input in inputs { - match input { - Input::Withdraw(input) => state = withdraw(state, input), - Input::Deposit(input) => state = deposit(state, input, pub_inputs), + state = match input { + Input::Withdraw(input) => state.withdraw(input), + Input::Deposit(input) => deposit(state, input, pub_inputs), } } - validate_zone_output(ptx_root, zone_in.input, zone_out, &state); + validate_zone_transition(zone_in, zone_out, in_meta, in_state_cm, state); + env::commit(&pub_inputs); } diff --git a/goas/cl/Cargo.toml b/goas/cl/Cargo.toml index c90b7d9..bcfe076 100644 --- a/goas/cl/Cargo.toml +++ b/goas/cl/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "cl", "ledger", "proof_statements", "risc0_proofs"] +members = [ "cl", "ledger", "ledger_proof_statements", "risc0_proofs"] # Always optimize; building and running the risc0_proofs takes much longer without optimization. [profile.dev] diff --git a/goas/cl/cl/src/balance.rs b/goas/cl/cl/src/balance.rs index 416a103..d88f921 100644 --- a/goas/cl/cl/src/balance.rs +++ b/goas/cl/cl/src/balance.rs @@ -40,6 +40,10 @@ impl BalanceWitness { Self(blinding) } + pub fn unblinded() -> Self { + Self::new(Scalar::ZERO) + } + pub fn random(mut rng: impl CryptoRngCore) -> Self { Self::new(Scalar::random(&mut rng)) } @@ -60,8 +64,8 @@ pub fn balance(value: u64, unit: Unit, blinding: Scalar) -> Unit { #[cfg(test)] mod test { - use super::*; + use crate::note::unit_point; #[test] fn test_pederson_blinding_point_pre_compute() { @@ -77,30 +81,34 @@ mod test { #[test] fn test_balance_zero_unitless() { // Zero is the same across all units + let (nmo, eth) = (unit_point("NMO"), unit_point("ETH")); + let mut rng = rand::thread_rng(); let b = BalanceWitness::random(&mut rng); assert_eq!( - b.commit(&NoteWitness::basic(0, "NMO")), - b.commit(&NoteWitness::basic(0, "ETH")), + b.commit(&NoteWitness::basic(0, nmo)), + b.commit(&NoteWitness::basic(0, eth)), ); } #[test] fn test_balance_blinding() { // balances are blinded + let nmo = unit_point("NMO"); + let r_a = Scalar::from(12u32); let r_b = Scalar::from(8u32); let bal_a = BalanceWitness::new(r_a); let bal_b = BalanceWitness::new(r_b); - let note = NoteWitness::basic(10, "NMO"); + let note = NoteWitness::basic(10, nmo); let a = bal_a.commit(¬e); let b = bal_b.commit(¬e); assert_ne!(a, b); - let diff_note = NoteWitness::basic(0, "NMO"); + let diff_note = NoteWitness::basic(0, nmo); assert_eq!( a.0 - b.0, BalanceWitness::new(r_a - r_b).commit(&diff_note).0 @@ -110,24 +118,28 @@ mod test { #[test] fn test_balance_units() { // Unit's differentiate between values. + let (nmo, eth) = (unit_point("NMO"), unit_point("ETH")); + let b = BalanceWitness::new(Scalar::from(1337u32)); - let nmo = NoteWitness::basic(10, "NMO"); - let eth = NoteWitness::basic(10, "ETH"); + let nmo = NoteWitness::basic(10, nmo); + let eth = NoteWitness::basic(10, eth); assert_ne!(b.commit(&nmo), b.commit(ð)); } #[test] fn test_balance_homomorphism() { + let nmo = unit_point("NMO"); + let mut rng = rand::thread_rng(); let b1 = BalanceWitness::random(&mut rng); let b2 = BalanceWitness::random(&mut rng); let b_zero = BalanceWitness::new(Scalar::ZERO); - let ten = NoteWitness::basic(10, "NMO"); - let eight = NoteWitness::basic(8, "NMO"); - let two = NoteWitness::basic(2, "NMO"); - let zero = NoteWitness::basic(0, "NMO"); + let ten = NoteWitness::basic(10, nmo); + let eight = NoteWitness::basic(8, nmo); + let two = NoteWitness::basic(2, nmo); + let zero = NoteWitness::basic(0, nmo); // Values of same unit are homomorphic assert_eq!( diff --git a/goas/cl/cl/src/bundle.rs b/goas/cl/cl/src/bundle.rs index e215f92..db9f238 100644 --- a/goas/cl/cl/src/bundle.rs +++ b/goas/cl/cl/src/bundle.rs @@ -29,7 +29,10 @@ impl Bundle { #[cfg(test)] mod test { use crate::{ - input::InputWitness, note::NoteWitness, nullifier::NullifierSecret, output::OutputWitness, + input::InputWitness, + note::{unit_point, NoteWitness}, + nullifier::NullifierSecret, + output::OutputWitness, partial_tx::PartialTxWitness, }; @@ -38,21 +41,22 @@ mod test { #[test] fn test_bundle_balance() { let mut rng = rand::thread_rng(); + let (nmo, eth, crv) = (unit_point("NMO"), unit_point("ETH"), unit_point("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::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(10, nmo), nf_a.commit(), &mut rng); let nmo_10_in = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); let eth_23_utxo = - OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(23, eth), nf_b.commit(), &mut rng); let eth_23_in = InputWitness::random(eth_23_utxo, nf_b, &mut rng); let crv_4840_out = - OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(4840, crv), nf_c.commit(), &mut rng); let ptx_unbalanced = PartialTxWitness { inputs: vec![nmo_10_in, eth_23_in], @@ -80,12 +84,12 @@ mod test { let crv_4840_in = InputWitness::random(crv_4840_out, nf_c, &mut rng); let nmo_10_out = OutputWitness::random( - NoteWitness::basic(10, "NMO"), + NoteWitness::basic(10, nmo), NullifierSecret::random(&mut rng).commit(), // transferring to a random owner &mut rng, ); let eth_23_out = OutputWitness::random( - NoteWitness::basic(23, "ETH"), + NoteWitness::basic(23, eth), NullifierSecret::random(&mut rng).commit(), // transferring to a random owner &mut rng, ); diff --git a/goas/cl/cl/src/input.rs b/goas/cl/cl/src/input.rs index af056a2..b1b9209 100644 --- a/goas/cl/cl/src/input.rs +++ b/goas/cl/cl/src/input.rs @@ -41,6 +41,17 @@ impl InputWitness { } } + 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 { + note: output.note, + balance_blinding: BalanceWitness::unblinded(), + nf_sk, + nonce: output.nonce, + } + } + pub fn evolved_nonce(&self) -> NullifierNonce { self.nonce.evolve(&self.nf_sk) } diff --git a/goas/cl/cl/src/lib.rs b/goas/cl/cl/src/lib.rs index 74d8dcf..43747ee 100644 --- a/goas/cl/cl/src/lib.rs +++ b/goas/cl/cl/src/lib.rs @@ -15,4 +15,6 @@ pub use input::{Input, InputWitness}; pub use note::{DeathCommitment, NoteCommitment, NoteWitness}; pub use nullifier::{Nullifier, NullifierCommitment, NullifierNonce, NullifierSecret}; pub use output::{Output, OutputWitness}; -pub use partial_tx::{PartialTx, PartialTxWitness, PtxRoot}; +pub use partial_tx::{ + PartialTx, PartialTxInputWitness, PartialTxOutputWitness, PartialTxWitness, PtxRoot, +}; diff --git a/goas/cl/cl/src/note.rs b/goas/cl/cl/src/note.rs index 644744c..18d0186 100644 --- a/goas/cl/cl/src/note.rs +++ b/goas/cl/cl/src/note.rs @@ -19,7 +19,7 @@ pub fn death_commitment(death_constraint: &[u8]) -> DeathCommitment { } pub fn unit_point(unit: &str) -> Unit { - crate::crypto::hash_to_curve(unit.as_bytes()) + crate::crypto::hash_to_curve(format!("NOMOS_CL_UNIT{unit}").as_bytes()) } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] @@ -42,25 +42,20 @@ pub struct NoteWitness { } impl NoteWitness { - pub fn new( - value: u64, - unit: impl Into, - death_constraint: [u8; 32], - state: [u8; 32], - ) -> Self { + pub fn new(value: u64, unit: Unit, death_constraint: [u8; 32], state: [u8; 32]) -> Self { Self { value, - unit: unit_point(&unit.into()), + unit, death_constraint, state, } } - pub fn basic(value: u64, unit: impl Into) -> Self { + pub fn basic(value: u64, unit: Unit) -> Self { Self::new(value, unit, [0u8; 32], [0u8; 32]) } - pub fn stateless(value: u64, unit: impl Into, death_constraint: [u8; 32]) -> Self { + pub fn stateless(value: u64, unit: Unit, death_constraint: [u8; 32]) -> Self { Self::new(value, unit, death_constraint, [0u8; 32]) } @@ -94,18 +89,19 @@ impl NoteWitness { #[cfg(test)] mod test { - use crate::nullifier::NullifierSecret; - use super::*; + use crate::nullifier::NullifierSecret; #[test] fn test_note_commit_permutations() { + let (nmo, eth) = (unit_point("NMO"), unit_point("ETH")); + let mut rng = rand::thread_rng(); let nf_pk = NullifierSecret::random(&mut rng).commit(); let nf_nonce = NullifierNonce::random(&mut rng); - let reference_note = NoteWitness::basic(32, "NMO"); + let reference_note = NoteWitness::basic(32, nmo); // different notes under same nullifier produce different commitments let mutation_tests = [ @@ -114,7 +110,7 @@ mod test { ..reference_note }, NoteWitness { - unit: unit_point("ETH"), + unit: eth, ..reference_note }, NoteWitness { diff --git a/goas/cl/cl/src/nullifier.rs b/goas/cl/cl/src/nullifier.rs index d5fb406..e8ba99f 100644 --- a/goas/cl/cl/src/nullifier.rs +++ b/goas/cl/cl/src/nullifier.rs @@ -43,6 +43,10 @@ impl NullifierSecret { 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"); diff --git a/goas/cl/cl/src/output.rs b/goas/cl/cl/src/output.rs index 1335c23..0e611ec 100644 --- a/goas/cl/cl/src/output.rs +++ b/goas/cl/cl/src/output.rs @@ -6,7 +6,7 @@ use crate::{ error::Error, note::{NoteCommitment, NoteWitness}, nullifier::{NullifierCommitment, NullifierNonce}, - BalanceWitness, + BalanceWitness, NullifierSecret, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -37,6 +37,15 @@ impl OutputWitness { } } + pub fn public(note: NoteWitness, nonce: NullifierNonce) -> Self { + Self { + note, + balance_blinding: BalanceWitness::unblinded(), + nf_pk: NullifierSecret::zero().commit(), + nonce, + } + } + pub fn commit_note(&self) -> NoteCommitment { self.note.commit(self.nf_pk, self.nonce) } @@ -86,14 +95,15 @@ impl Output { #[cfg(test)] mod test { use super::*; - use crate::nullifier::NullifierSecret; + use crate::{note::unit_point, nullifier::NullifierSecret}; #[test] fn test_output_proof() { + let (nmo, eth) = (unit_point("NMO"), unit_point("ETH")); let mut rng = rand::thread_rng(); let witness = OutputWitness { - note: NoteWitness::basic(10, "NMO"), + note: NoteWitness::basic(10, nmo), balance_blinding: BalanceWitness::random(&mut rng), nf_pk: NullifierSecret::random(&mut rng).commit(), nonce: NullifierNonce::random(&mut rng), @@ -106,11 +116,11 @@ mod test { let wrong_witnesses = [ OutputWitness { - note: NoteWitness::basic(11, "NMO"), + note: NoteWitness::basic(11, nmo), ..witness }, OutputWitness { - note: NoteWitness::basic(10, "ETH"), + note: NoteWitness::basic(10, eth), ..witness }, OutputWitness { diff --git a/goas/cl/cl/src/partial_tx.rs b/goas/cl/cl/src/partial_tx.rs index 5610421..4854cd9 100644 --- a/goas/cl/cl/src/partial_tx.rs +++ b/goas/cl/cl/src/partial_tx.rs @@ -60,6 +60,26 @@ impl PartialTxWitness { BalanceWitness(out_sum - in_sum) } + + pub fn input_witness(&self, idx: usize) -> PartialTxInputWitness { + let input_bytes = + Vec::from_iter(self.inputs.iter().map(|i| i.commit().to_bytes().to_vec())); + let input_merkle_leaves = merkle::padded_leaves::(&input_bytes); + + let path = merkle::path(input_merkle_leaves, idx); + let input = self.inputs[idx]; + PartialTxInputWitness { input, path } + } + + pub fn output_witness(&self, idx: usize) -> PartialTxOutputWitness { + let output_bytes = + Vec::from_iter(self.outputs.iter().map(|o| o.commit().to_bytes().to_vec())); + let output_merkle_leaves = merkle::padded_leaves::(&output_bytes); + + let path = merkle::path(output_merkle_leaves, idx); + let output = self.outputs[idx]; + PartialTxOutputWitness { output, path } + } } impl PartialTx { @@ -81,24 +101,6 @@ impl PartialTx { merkle::root::(output_merkle_leaves) } - pub fn input_merkle_path(&self, idx: usize) -> Vec { - 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::path(input_merkle_leaves, idx) - } - - pub fn output_merkle_path(&self, idx: usize) -> Vec { - 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::path(output_merkle_leaves, idx) - } - pub fn root(&self) -> PtxRoot { let input_root = self.input_root(); let output_root = self.output_root(); @@ -114,15 +116,47 @@ impl PartialTx { } } +/// An input to a partial transaction +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxInputWitness { + pub input: InputWitness, + pub path: Vec, +} + +impl PartialTxInputWitness { + pub fn input_root(&self) -> [u8; 32] { + let leaf = merkle::leaf(&self.input.commit().to_bytes()); + merkle::path_root(leaf, &self.path) + } +} + +/// An output to a partial transaction +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PartialTxOutputWitness { + pub output: OutputWitness, + pub path: Vec, +} + +impl PartialTxOutputWitness { + pub fn output_root(&self) -> [u8; 32] { + let leaf = merkle::leaf(&self.output.commit().to_bytes()); + merkle::path_root(leaf, &self.path) + } +} + #[cfg(test)] mod test { - use crate::{note::NoteWitness, nullifier::NullifierSecret}; + use crate::{ + note::{unit_point, NoteWitness}, + nullifier::NullifierSecret, + }; use super::*; #[test] fn test_partial_tx_balance() { + let (nmo, eth, crv) = (unit_point("NMO"), unit_point("ETH"), unit_point("CRV")); let mut rng = rand::thread_rng(); let nf_a = NullifierSecret::random(&mut rng); @@ -130,15 +164,15 @@ mod test { let nf_c = NullifierSecret::random(&mut rng); let nmo_10_utxo = - OutputWitness::random(NoteWitness::basic(10, "NMO"), nf_a.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(10, nmo), nf_a.commit(), &mut rng); let nmo_10 = InputWitness::random(nmo_10_utxo, nf_a, &mut rng); let eth_23_utxo = - OutputWitness::random(NoteWitness::basic(23, "ETH"), nf_b.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(23, eth), nf_b.commit(), &mut rng); let eth_23 = InputWitness::random(eth_23_utxo, nf_b, &mut rng); let crv_4840 = - OutputWitness::random(NoteWitness::basic(4840, "CRV"), nf_c.commit(), &mut rng); + OutputWitness::random(NoteWitness::basic(4840, crv), nf_c.commit(), &mut rng); let ptx_witness = PartialTxWitness { inputs: vec![nmo_10, eth_23], diff --git a/goas/cl/cl/tests/simple_transfer.rs b/goas/cl/cl/tests/simple_transfer.rs index f008b6f..69b15c4 100644 --- a/goas/cl/cl/tests/simple_transfer.rs +++ b/goas/cl/cl/tests/simple_transfer.rs @@ -1,3 +1,4 @@ +use cl::note::unit_point; use rand_core::CryptoRngCore; fn receive_utxo( @@ -10,6 +11,7 @@ fn receive_utxo( #[test] fn test_simple_transfer() { + let nmo = unit_point("NMO"); let mut rng = rand::thread_rng(); let sender_nf_sk = cl::NullifierSecret::random(&mut rng); @@ -18,13 +20,13 @@ fn test_simple_transfer() { 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"), sender_nf_pk, &mut rng); + let utxo = receive_utxo(cl::NoteWitness::basic(10, nmo), sender_nf_pk, &mut rng); // and wants to send 8 NMO to some recipient and return 2 NMO to itself. let recipient_output = - cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), recipient_nf_pk, &mut rng); + cl::OutputWitness::random(cl::NoteWitness::basic(8, nmo), recipient_nf_pk, &mut rng); let change_output = - cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), sender_nf_pk, &mut rng); + cl::OutputWitness::random(cl::NoteWitness::basic(2, nmo), sender_nf_pk, &mut rng); let ptx_witness = cl::PartialTxWitness { inputs: vec![cl::InputWitness::random(utxo, sender_nf_sk, &mut rng)], diff --git a/goas/cl/ledger/Cargo.toml b/goas/cl/ledger/Cargo.toml index 0dc7269..ac37f56 100644 --- a/goas/cl/ledger/Cargo.toml +++ b/goas/cl/ledger/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] cl = { path = "../cl" } -proof_statements = { path = "../proof_statements" } +ledger_proof_statements = { path = "../ledger_proof_statements" } nomos_cl_risc0_proofs = { path = "../risc0_proofs" } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-groth16 = { version = "1.0" } diff --git a/goas/cl/ledger/src/death_constraint.rs b/goas/cl/ledger/src/death_constraint.rs index e00972c..9138a43 100644 --- a/goas/cl/ledger/src/death_constraint.rs +++ b/goas/cl/ledger/src/death_constraint.rs @@ -1,4 +1,4 @@ -use proof_statements::death_constraint::DeathConstraintPublic; +use ledger_proof_statements::death_constraint::DeathConstraintPublic; use sha2::{Digest, Sha256}; use crate::error::Result; @@ -7,11 +7,11 @@ pub type Risc0DeathConstraintId = [u32; 8]; #[derive(Debug, Clone)] pub struct DeathProof { - constraint: Risc0DeathConstraintId, - risc0_receipt: risc0_zkvm::Receipt, + pub constraint: Risc0DeathConstraintId, + pub risc0_receipt: risc0_zkvm::Receipt, } -fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] { +pub fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] { // RISC0 proof ids have the format: [u32; 8], and cl death constraint ids have the format [u8; 32]. // CL death constraints are meaningless beyond being binding, therefore we merely need a collision // resisitant mapping of RISC0 ids to cl death constraints. @@ -26,6 +26,16 @@ fn risc0_id_to_cl_death_constraint(risc0_id: Risc0DeathConstraintId) -> [u8; 32] } impl DeathProof { + pub fn from_risc0( + risc0_id: Risc0DeathConstraintId, + risc0_receipt: risc0_zkvm::Receipt, + ) -> Self { + Self { + constraint: risc0_id, + risc0_receipt, + } + } + pub fn death_commitment(&self) -> cl::DeathCommitment { cl::note::death_commitment(&risc0_id_to_cl_death_constraint(self.constraint)) } @@ -75,9 +85,6 @@ impl DeathProof { // extract the receipt. let receipt = prove_info.receipt; - Self { - constraint: nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID, - risc0_receipt: receipt, - } + Self::from_risc0(nomos_cl_risc0_proofs::DEATH_CONSTRAINT_NOP_ID, receipt) } } diff --git a/goas/cl/ledger/src/input.rs b/goas/cl/ledger/src/input.rs index 87000ad..1c3b9b1 100644 --- a/goas/cl/ledger/src/input.rs +++ b/goas/cl/ledger/src/input.rs @@ -1,4 +1,4 @@ -use proof_statements::input::{InputPrivate, InputPublic}; +use ledger_proof_statements::input::{InputPrivate, InputPublic}; use crate::error::{Error, Result}; @@ -89,16 +89,18 @@ fn note_commitment_leaves(note_commitments: &[cl::NoteCommitment]) -> [[u8; 32]; #[cfg(test)] mod test { - use rand::thread_rng; - use super::*; + use cl::note::unit_point; + use rand::thread_rng; #[test] fn test_input_prover() { + let nmo = unit_point("NMO"); + let mut rng = thread_rng(); let input = cl::InputWitness { - note: cl::NoteWitness::basic(32, "NMO"), + note: cl::NoteWitness::basic(32, nmo), balance_blinding: cl::BalanceWitness::random(&mut rng), nf_sk: cl::NullifierSecret::random(&mut rng), nonce: cl::NullifierNonce::random(&mut rng), @@ -141,7 +143,7 @@ mod test { InputPublic { input: cl::Input { balance: cl::BalanceWitness::random(&mut rng) - .commit(&cl::NoteWitness::basic(32, "NMO")), + .commit(&cl::NoteWitness::basic(32, nmo)), ..expected_public_inputs.input }, ..expected_public_inputs diff --git a/goas/cl/ledger/src/lib.rs b/goas/cl/ledger/src/lib.rs index 115f111..d5b1744 100644 --- a/goas/cl/ledger/src/lib.rs +++ b/goas/cl/ledger/src/lib.rs @@ -4,3 +4,5 @@ pub mod error; pub mod input; pub mod output; pub mod partial_tx; + +pub use death_constraint::DeathProof; diff --git a/goas/cl/ledger/src/output.rs b/goas/cl/ledger/src/output.rs index d3fd71b..89a9238 100644 --- a/goas/cl/ledger/src/output.rs +++ b/goas/cl/ledger/src/output.rs @@ -55,16 +55,18 @@ impl ProvedOutput { #[cfg(test)] mod test { - use rand::thread_rng; - use super::*; + use cl::note::unit_point; + use rand::thread_rng; #[test] fn test_output_prover() { + let nmo = unit_point("NMO"); + let mut rng = thread_rng(); let output = cl::OutputWitness { - note: cl::NoteWitness::basic(32, "NMO"), + note: cl::NoteWitness::basic(32, nmo), balance_blinding: cl::BalanceWitness::random(&mut rng), nf_pk: cl::NullifierSecret::random(&mut rng).commit(), nonce: cl::NullifierNonce::random(&mut rng), @@ -79,19 +81,19 @@ mod test { let wrong_output_cms = [ cl::Output { - note_comm: cl::NoteWitness::basic(100, "NMO").commit( + note_comm: cl::NoteWitness::basic(100, nmo).commit( cl::NullifierSecret::random(&mut rng).commit(), cl::NullifierNonce::random(&mut rng), ), ..expected_output_cm }, cl::Output { - note_comm: cl::NoteWitness::basic(100, "NMO").commit( + note_comm: cl::NoteWitness::basic(100, nmo).commit( cl::NullifierSecret::random(&mut rng).commit(), cl::NullifierNonce::random(&mut rng), ), balance: cl::BalanceWitness::random(&mut rng) - .commit(&cl::NoteWitness::basic(100, "NMO")), + .commit(&cl::NoteWitness::basic(100, nmo)), }, ]; @@ -103,10 +105,11 @@ mod test { #[test] fn test_zero_output_is_rejected() { + let nmo = unit_point("NMO"); let mut rng = thread_rng(); let output = cl::OutputWitness::random( - cl::NoteWitness::basic(0, "NMO"), + cl::NoteWitness::basic(0, nmo), cl::NullifierSecret::random(&mut rng).commit(), &mut rng, ); diff --git a/goas/cl/ledger/src/partial_tx.rs b/goas/cl/ledger/src/partial_tx.rs index 998b729..9173519 100644 --- a/goas/cl/ledger/src/partial_tx.rs +++ b/goas/cl/ledger/src/partial_tx.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use proof_statements::death_constraint::DeathConstraintPublic; +use ledger_proof_statements::death_constraint::DeathConstraintPublic; use crate::{ death_constraint::DeathProof, error::Result, input::ProvedInput, output::ProvedOutput, diff --git a/goas/cl/ledger/tests/simple_transfer.rs b/goas/cl/ledger/tests/simple_transfer.rs index 9d83f48..79b70a3 100644 --- a/goas/cl/ledger/tests/simple_transfer.rs +++ b/goas/cl/ledger/tests/simple_transfer.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use cl::note::unit_point; use ledger::{bundle::ProvedBundle, death_constraint::DeathProof, partial_tx::ProvedPartialTx}; use rand_core::CryptoRngCore; @@ -29,6 +30,8 @@ fn receive_utxo( #[test] fn test_simple_transfer() { + let nmo = unit_point("NMO"); + let mut rng = rand::thread_rng(); // alice is sending 8 NMO to bob. @@ -38,19 +41,18 @@ fn test_simple_transfer() { // Alice has an unspent note worth 10 NMO let utxo = receive_utxo( - cl::NoteWitness::stateless(10, "NMO", DeathProof::nop_constraint()), + cl::NoteWitness::stateless(10, nmo, DeathProof::nop_constraint()), alice.pk(), &mut rng, ); let alices_input = cl::InputWitness::random(utxo, alice.sk(), &mut rng); // Alice wants to send 8 NMO to bob - let bobs_output = - cl::OutputWitness::random(cl::NoteWitness::basic(8, "NMO"), bob.pk(), &mut rng); + let bobs_output = cl::OutputWitness::random(cl::NoteWitness::basic(8, nmo), bob.pk(), &mut rng); // .. and return the 2 NMO in change to herself. let change_output = - cl::OutputWitness::random(cl::NoteWitness::basic(2, "NMO"), alice.pk(), &mut rng); + cl::OutputWitness::random(cl::NoteWitness::basic(2, nmo), alice.pk(), &mut rng); // Construct the ptx consuming Alices inputs and producing the two outputs. let ptx_witness = cl::PartialTxWitness { diff --git a/goas/cl/proof_statements/Cargo.toml b/goas/cl/ledger_proof_statements/Cargo.toml similarity index 80% rename from goas/cl/proof_statements/Cargo.toml rename to goas/cl/ledger_proof_statements/Cargo.toml index d3b82ad..65ea695 100644 --- a/goas/cl/proof_statements/Cargo.toml +++ b/goas/cl/ledger_proof_statements/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "proof_statements" +name = "ledger_proof_statements" version = "0.1.0" edition = "2021" diff --git a/goas/cl/proof_statements/src/death_constraint.rs b/goas/cl/ledger_proof_statements/src/death_constraint.rs similarity index 100% rename from goas/cl/proof_statements/src/death_constraint.rs rename to goas/cl/ledger_proof_statements/src/death_constraint.rs diff --git a/goas/cl/proof_statements/src/input.rs b/goas/cl/ledger_proof_statements/src/input.rs similarity index 100% rename from goas/cl/proof_statements/src/input.rs rename to goas/cl/ledger_proof_statements/src/input.rs diff --git a/goas/cl/proof_statements/src/lib.rs b/goas/cl/ledger_proof_statements/src/lib.rs similarity index 75% rename from goas/cl/proof_statements/src/lib.rs rename to goas/cl/ledger_proof_statements/src/lib.rs index 8e58650..1029da4 100644 --- a/goas/cl/proof_statements/src/lib.rs +++ b/goas/cl/ledger_proof_statements/src/lib.rs @@ -1,3 +1,2 @@ pub mod death_constraint; pub mod input; -pub mod ptx; diff --git a/goas/cl/proof_statements/src/ptx.rs b/goas/cl/proof_statements/src/ptx.rs deleted file mode 100644 index 9701646..0000000 --- a/goas/cl/proof_statements/src/ptx.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cl::{merkle, InputWitness, OutputWitness, PtxRoot}; -use serde::{Deserialize, Serialize}; - -/// An input to a partial transaction -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PartialTxInputPrivate { - pub input: InputWitness, - pub cm_path: Vec, - pub ptx_path: Vec, -} - -impl PartialTxInputPrivate { - pub fn ptx_root(&self) -> PtxRoot { - let leaf = merkle::leaf(&self.input.commit().to_bytes()); - PtxRoot(merkle::path_root(leaf, &self.ptx_path)) - } - - pub fn cm_root(&self) -> [u8; 32] { - let leaf = merkle::leaf(self.input.note_commitment().as_bytes()); - merkle::path_root(leaf, &self.cm_path) - } -} - -/// An output to a partial transaction -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PartialTxOutputPrivate { - pub output: OutputWitness, - pub ptx_path: Vec, -} - -impl PartialTxOutputPrivate { - pub fn ptx_root(&self) -> PtxRoot { - let leaf = merkle::leaf(&self.output.commit().to_bytes()); - PtxRoot(merkle::path_root(leaf, &self.ptx_path)) - } -} diff --git a/goas/cl/risc0_proofs/bundle/Cargo.toml b/goas/cl/risc0_proofs/bundle/Cargo.toml index a2a0124..f207737 100644 --- a/goas/cl/risc0_proofs/bundle/Cargo.toml +++ b/goas/cl/risc0_proofs/bundle/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } -proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } [patch.crates-io] diff --git a/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml b/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml index 46cab6a..5b7136a 100644 --- a/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml +++ b/goas/cl/risc0_proofs/death_constraint_nop/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } -proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } [patch.crates-io] diff --git a/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs b/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs index 09920ad..232c447 100644 --- a/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs +++ b/goas/cl/risc0_proofs/death_constraint_nop/src/main.rs @@ -1,5 +1,5 @@ /// Death Constraint No-op Proof -use proof_statements::death_constraint::DeathConstraintPublic; +use ledger_proof_statements::death_constraint::DeathConstraintPublic; use risc0_zkvm::guest::env; fn main() { diff --git a/goas/cl/risc0_proofs/input/Cargo.toml b/goas/cl/risc0_proofs/input/Cargo.toml index f39def3..fd1f63f 100644 --- a/goas/cl/risc0_proofs/input/Cargo.toml +++ b/goas/cl/risc0_proofs/input/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } -proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } [patch.crates-io] diff --git a/goas/cl/risc0_proofs/input/src/main.rs b/goas/cl/risc0_proofs/input/src/main.rs index baa0737..ada2145 100644 --- a/goas/cl/risc0_proofs/input/src/main.rs +++ b/goas/cl/risc0_proofs/input/src/main.rs @@ -1,6 +1,6 @@ /// Input Proof use cl::merkle; -use proof_statements::input::{InputPrivate, InputPublic}; +use ledger_proof_statements::input::{InputPrivate, InputPublic}; use risc0_zkvm::guest::env; fn main() { diff --git a/goas/cl/risc0_proofs/output/Cargo.toml b/goas/cl/risc0_proofs/output/Cargo.toml index 6b4f0f9..e33aea6 100644 --- a/goas/cl/risc0_proofs/output/Cargo.toml +++ b/goas/cl/risc0_proofs/output/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" risc0-zkvm = { version = "1.0", default-features = false, features = ['std'] } serde = { version = "1.0", features = ["derive"] } cl = { path = "../../cl" } -proof_statements = { path = "../../proof_statements" } +ledger_proof_statements = { path = "../../ledger_proof_statements" } [patch.crates-io]