diff --git a/goas/atomic_asset_transfer/Cargo.toml b/goas/atomic_asset_transfer/Cargo.toml index 457d42f..b8d95ee 100644 --- a/goas/atomic_asset_transfer/Cargo.toml +++ b/goas/atomic_asset_transfer/Cargo.toml @@ -8,4 +8,4 @@ opt-level = 3 [profile.release] debug = 1 -lto = true \ No newline at end of file +lto = true diff --git a/goas/atomic_asset_transfer/common/Cargo.toml b/goas/atomic_asset_transfer/common/Cargo.toml index 1308a01..52793c2 100644 --- a/goas/atomic_asset_transfer/common/Cargo.toml +++ b/goas/atomic_asset_transfer/common/Cargo.toml @@ -9,3 +9,7 @@ cl = { path = "../../cl/cl" } ledger_proof_statements = { path = "../../cl/ledger_proof_statements" } once_cell = "1" sha2 = "0.10" +curve25519-dalek = { version = "4.1", features = ["serde", "digest", "rand_core"] } +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +rand_core = "0.6.0" +serde_arrays = "0.1.0" diff --git a/goas/atomic_asset_transfer/common/src/lib.rs b/goas/atomic_asset_transfer/common/src/lib.rs index c76a3c6..6c10c04 100644 --- a/goas/atomic_asset_transfer/common/src/lib.rs +++ b/goas/atomic_asset_transfer/common/src/lib.rs @@ -1,5 +1,10 @@ -use cl::{balance::Unit, merkle, PartialTxInputWitness}; +use cl::{balance::Unit, merkle, NoteCommitment}; +use ed25519_dalek::{ + ed25519::{signature::SignerMut, SignatureBytes}, + Signature, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, +}; use once_cell::sync::Lazy; +use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; @@ -13,11 +18,15 @@ pub const MAX_EVENTS: usize = 1 << 8; #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct StateCommitment(pub [u8; 32]); -pub type AccountId = u32; +pub type AccountId = [u8; PUBLIC_KEY_LENGTH]; // PLACEHOLDER: this is probably going to be NMO? pub static ZONE_CL_FUNDS_UNIT: Lazy = Lazy::new(|| cl::note::unit_point("NMO")); +pub fn new_account(mut rng: impl CryptoRngCore) -> SigningKey { + SigningKey::generate(&mut rng) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ZoneMetadata { pub zone_vk: [u8; 32], @@ -28,8 +37,8 @@ pub struct ZoneMetadata { impl ZoneMetadata { pub fn id(&self) -> [u8; 32] { let mut hasher = Sha256::new(); - hasher.update(&self.zone_vk); - hasher.update(&self.funds_vk); + hasher.update(self.zone_vk); + hasher.update(self.funds_vk); hasher.update(self.unit.compress().as_bytes()); hasher.finalize().into() } @@ -40,7 +49,6 @@ pub struct StateWitness { pub balances: BTreeMap, pub included_txs: Vec, pub zone_metadata: ZoneMetadata, - pub nonce: [u8; 32], } impl StateWitness { @@ -50,14 +58,24 @@ impl StateWitness { pub fn state_roots(&self) -> StateRoots { StateRoots { - nonce: self.nonce, tx_root: self.included_txs_root(), zone_id: self.zone_metadata.id(), balance_root: self.balances_root(), } } - pub fn withdraw(mut self, w: Withdraw) -> Self { + pub fn apply(self, tx: Tx) -> Self { + let mut state = match tx { + Tx::Withdraw(w) => self.withdraw(w), + Tx::Deposit(d) => self.deposit(d), + }; + + state.included_txs.push(tx); + + state + } + + fn withdraw(mut self, w: Withdraw) -> Self { let Withdraw { from, amount } = w; let from_balance = self.balances.entry(from).or_insert(0); @@ -65,12 +83,10 @@ impl StateWitness { .checked_sub(amount) .expect("insufficient funds in account"); - self.included_txs.push(Tx::Withdraw(w)); - self } - pub fn deposit(mut self, d: Deposit) -> Self { + fn deposit(mut self, d: Deposit) -> Self { let Deposit { to, amount } = d; let to_balance = self.balances.entry(to).or_insert(0); @@ -78,7 +94,6 @@ impl StateWitness { .checked_add(amount) .expect("overflow in account balance"); - self.included_txs.push(Tx::Deposit(d)); self } @@ -87,7 +102,7 @@ impl StateWitness { } pub fn included_tx_witness(&self, idx: usize) -> IncludedTxWitness { - let tx = self.included_txs.get(idx).unwrap().clone(); + let tx = *self.included_txs.get(idx).unwrap(); let path = merkle::path(self.included_tx_merkle_leaves(), idx); IncludedTxWitness { tx, path } } @@ -95,7 +110,7 @@ impl StateWitness { 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(owner); bytes.extend(balance.to_le_bytes()); bytes })); @@ -107,19 +122,6 @@ impl StateWitness { self.balances.values().sum() } - pub fn evolve_nonce(self) -> Self { - let updated_nonce = { - let mut hasher = Sha256::new(); - hasher.update(&self.nonce); - hasher.update(b"NOMOS_ZONE_NONCE_EVOLVE"); - hasher.finalize().into() - }; - Self { - nonce: updated_nonce, - ..self - } - } - fn included_tx_merkle_leaves(&self) -> [[u8; 32]; MAX_TXS] { let tx_bytes = self .included_txs @@ -143,10 +145,10 @@ pub struct Withdraw { } impl Withdraw { - pub fn to_bytes(&self) -> [u8; 12] { - let mut bytes = [0; 12]; - bytes[0..4].copy_from_slice(&self.from.to_le_bytes()); - bytes[4..12].copy_from_slice(&self.amount.to_le_bytes()); + pub fn to_bytes(&self) -> [u8; 40] { + let mut bytes = [0; 40]; + bytes[0..PUBLIC_KEY_LENGTH].copy_from_slice(&self.from); + bytes[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + 8].copy_from_slice(&self.amount.to_le_bytes()); bytes } } @@ -159,31 +161,58 @@ pub struct Deposit { } impl Deposit { - pub fn to_bytes(&self) -> [u8; 12] { - let mut bytes = [0; 12]; - bytes[0..4].copy_from_slice(&self.to.to_le_bytes()); - bytes[4..12].copy_from_slice(&self.amount.to_le_bytes()); + pub fn to_bytes(&self) -> [u8; 40] { + let mut bytes = [0; 40]; + bytes[0..PUBLIC_KEY_LENGTH].copy_from_slice(&self.to); + bytes[PUBLIC_KEY_LENGTH..PUBLIC_KEY_LENGTH + 8].copy_from_slice(&self.amount.to_le_bytes()); bytes } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedBoundTx { + pub bound_tx: BoundTx, + #[serde(with = "serde_arrays")] + pub sig: SignatureBytes, +} + +impl SignedBoundTx { + pub fn sign(bound_tx: BoundTx, signing_key: &mut SigningKey) -> Self { + let msg = bound_tx.to_bytes(); + let sig = signing_key.sign(&msg).to_bytes(); + + Self { bound_tx, sig } + } + + pub fn verify_and_unwrap(&self) -> BoundTx { + let msg = self.bound_tx.to_bytes(); + + let sig = Signature::from_bytes(&self.sig); + let vk = self.bound_tx.tx.verifying_key(); + vk.verify_strict(&msg, &sig).expect("Invalid tx signature"); + + self.bound_tx + } +} + /// A Tx that is executed in the zone if and only if the bind is /// present is the same partial transaction -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct BoundTx { pub tx: Tx, - pub bind: PartialTxInputWitness, + pub bind: NoteCommitment, } impl BoundTx { pub fn to_bytes(&self) -> Vec { - let mut bytes = self.tx.to_bytes(); - bytes.extend(self.bind.input.commit().to_bytes()); + let mut bytes = Vec::new(); + bytes.extend(self.tx.to_bytes()); + bytes.extend(self.bind.as_bytes()); bytes } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Tx { Withdraw(Withdraw), Deposit(Deposit), @@ -196,6 +225,13 @@ impl Tx { Tx::Deposit(deposit) => deposit.to_bytes().to_vec(), } } + + pub fn verifying_key(&self) -> VerifyingKey { + match self { + Tx::Withdraw(w) => VerifyingKey::from_bytes(&w.from).unwrap(), + Tx::Deposit(d) => VerifyingKey::from_bytes(&d.to).unwrap(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -213,25 +249,19 @@ impl IncludedTxWitness { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StateRoots { - pub nonce: [u8; 32], pub tx_root: [u8; 32], pub zone_id: [u8; 32], pub balance_root: [u8; 32], } impl StateRoots { - /// Merkle tree over: - /// root - /// / \ - /// io state - /// / \ / \ - /// nonce txs zoneid balances + /// Merkle tree over: [txs, zoneid, balances] pub fn commit(&self) -> StateCommitment { - StateCommitment(cl::merkle::root([ - self.nonce, - self.tx_root, - self.zone_id, - self.balance_root, - ])) + let leaves = cl::merkle::padded_leaves::<4>(&[ + self.tx_root.to_vec(), + self.zone_id.to_vec(), + self.balance_root.to_vec(), + ]); + StateCommitment(cl::merkle::root(leaves)) } } diff --git a/goas/atomic_asset_transfer/executor/src/lib.rs b/goas/atomic_asset_transfer/executor/src/lib.rs index 7565f79..3bb8de4 100644 --- a/goas/atomic_asset_transfer/executor/src/lib.rs +++ b/goas/atomic_asset_transfer/executor/src/lib.rs @@ -1,5 +1,105 @@ -use common::{BoundTx, StateWitness, ZoneMetadata}; -use goas_proof_statements::{zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate}; +use std::collections::BTreeMap; + +use common::{AccountId, SignedBoundTx, StateWitness, Tx, ZoneMetadata}; +use goas_proof_statements::{ + user_note::UserAtomicTransfer, zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate, +}; +use rand_core::CryptoRngCore; + +#[derive(Debug, Clone)] +pub struct ZoneNotes { + pub state: StateWitness, + pub state_note: cl::OutputWitness, + pub fund_note: cl::OutputWitness, +} + +impl ZoneNotes { + pub fn new_with_balances( + zone_name: &str, + balances: BTreeMap, + mut rng: impl CryptoRngCore, + ) -> Self { + let state = StateWitness { + balances, + included_txs: vec![], + zone_metadata: zone_metadata(zone_name), + }; + let state_note = zone_state_utxo(&state, &mut rng); + let fund_note = zone_fund_utxo(state.total_balance(), state.zone_metadata, &mut rng); + Self { + state, + state_note, + fund_note, + } + } + + pub fn state_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.state_note) + } + + pub fn fund_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.fund_note) + } + + pub fn run(mut self, txs: impl IntoIterator) -> Self { + for tx in txs { + self.state = self.state.apply(tx); + } + + let state_in = self.state_input_witness(); + self.state_note = cl::OutputWitness::public( + cl::NoteWitness { + state: self.state.commit().0, + ..state_in.note + }, + state_in.evolved_nonce(b"STATE_NONCE"), + ); + + let fund_in = self.fund_input_witness(); + self.fund_note = cl::OutputWitness::public( + cl::NoteWitness { + value: self.state.total_balance(), + ..fund_in.note + }, + state_in.evolved_nonce(b"FUND_NONCE"), + ); + self + } +} + +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(), + }, + cl::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, + }, + cl::NullifierNonce::random(&mut rng), + ) +} + +pub fn user_atomic_transfer_death_constraint() -> [u8; 32] { + ledger::death_constraint::risc0_id_to_cl_death_constraint( + goas_risc0_proofs::USER_ATOMIC_TRANSFER_ID, + ) +} pub fn zone_state_death_constraint() -> [u8; 32] { ledger::death_constraint::risc0_id_to_cl_death_constraint(goas_risc0_proofs::ZONE_STATE_ID) @@ -21,7 +121,7 @@ pub fn zone_metadata(zone_mnemonic: &str) -> ZoneMetadata { pub fn prove_zone_stf( state: StateWitness, - inputs: Vec, + inputs: Vec<(SignedBoundTx, cl::PartialTxInputWitness)>, zone_in: cl::PartialTxInputWitness, zone_out: cl::PartialTxOutputWitness, funds_out: cl::PartialTxOutputWitness, @@ -48,7 +148,11 @@ pub fn prove_zone_stf( 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()); + println!( + "STARK 'zone_stf' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); let receipt = prove_info.receipt; ledger::DeathProof::from_risc0(goas_risc0_proofs::ZONE_STATE_ID, receipt) } @@ -78,7 +182,35 @@ pub fn prove_zone_fund_withdraw( 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()); + println!( + "STARK 'zone_fund' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); let receipt = prove_info.receipt; ledger::DeathProof::from_risc0(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, receipt) } + +pub fn prove_user_atomic_transfer(atomic_transfer: UserAtomicTransfer) -> ledger::DeathProof { + let env = risc0_zkvm::ExecutorEnv::builder() + .write(&atomic_transfer) + .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::USER_ATOMIC_TRANSFER_ELF, &opts) + .unwrap(); + println!( + "STARK 'user atomic transfer' prover time: {:.2?}, total_cycles: {}", + start_t.elapsed(), + prove_info.stats.total_cycles + ); + let receipt = prove_info.receipt; + ledger::DeathProof::from_risc0(goas_risc0_proofs::USER_ATOMIC_TRANSFER_ID, receipt) +} diff --git a/goas/atomic_asset_transfer/executor/tests/atomic_transfer.rs b/goas/atomic_asset_transfer/executor/tests/atomic_transfer.rs new file mode 100644 index 0000000..ac66029 --- /dev/null +++ b/goas/atomic_asset_transfer/executor/tests/atomic_transfer.rs @@ -0,0 +1,176 @@ +use std::collections::BTreeMap; + +use cl::{BundleWitness, NoteWitness, NullifierNonce}; +use common::{new_account, BoundTx, Deposit, SignedBoundTx, Tx, Withdraw}; +use executor::ZoneNotes; +use goas_proof_statements::user_note::{UserAtomicTransfer, UserIntent}; + +#[test] +fn test_atomic_transfer() { + let mut rng = rand::thread_rng(); + + let mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + + let zone_a_start = + ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice_vk, 100)]), &mut rng); + + let zone_b_start = ZoneNotes::new_with_balances("ZONE_B", BTreeMap::from_iter([]), &mut rng); + + let alice_intent = UserIntent { + zone_a_meta: zone_a_start.state.zone_metadata, + zone_b_meta: zone_b_start.state.zone_metadata, + withdraw: Withdraw { + from: alice_vk, + amount: 75, + }, + deposit: Deposit { + to: alice_vk, + amount: 75, + }, + }; + + let alice_intent_out = cl::OutputWitness::public( + NoteWitness { + value: 1, + unit: cl::note::unit_point("INTENT"), + death_constraint: executor::user_atomic_transfer_death_constraint(), + state: alice_intent.commit(), + }, + NullifierNonce::random(&mut rng), + ); + + let user_ptx = cl::PartialTxWitness { + inputs: vec![], + outputs: vec![alice_intent_out], + }; + + let zone_a_end = zone_a_start + .clone() + .run([Tx::Withdraw(alice_intent.withdraw)]); + + let zone_b_end = zone_b_start + .clone() + .run([Tx::Deposit(alice_intent.deposit)]); + + let alice_intent_in = cl::InputWitness::public(alice_intent_out); + let atomic_transfer_ptx = cl::PartialTxWitness { + inputs: vec![ + alice_intent_in, + zone_a_start.state_input_witness(), + zone_a_start.fund_input_witness(), + zone_b_start.state_input_witness(), + zone_b_start.fund_input_witness(), + ], + outputs: vec![ + zone_a_end.state_note, + zone_a_end.fund_note, + zone_b_end.state_note, + zone_b_end.fund_note, + ], + }; + + let signed_withdraw = SignedBoundTx::sign( + BoundTx { + tx: Tx::Withdraw(alice_intent.withdraw), + bind: alice_intent_in.note_commitment(), + }, + &mut alice, + ); + let signed_deposit = SignedBoundTx::sign( + BoundTx { + tx: Tx::Deposit(alice_intent.deposit), + bind: alice_intent_in.note_commitment(), + }, + &mut alice, + ); + + let death_proofs = BTreeMap::from_iter([ + ( + alice_intent_in.nullifier(), + executor::prove_user_atomic_transfer(UserAtomicTransfer { + user_note: atomic_transfer_ptx.input_witness(0), + user_intent: alice_intent, + zone_a: atomic_transfer_ptx.output_witness(0), + zone_b: atomic_transfer_ptx.output_witness(2), + zone_a_roots: zone_a_end.state.state_roots(), + zone_b_roots: zone_b_end.state.state_roots(), + withdraw_tx: zone_a_end.state.included_tx_witness(0), + deposit_tx: zone_b_end.state.included_tx_witness(0), + }), + ), + ( + zone_a_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_a_start.state.clone(), + vec![(signed_withdraw, atomic_transfer_ptx.input_witness(0))], // withdraw bound to input intent note + atomic_transfer_ptx.input_witness(1), // input state note + atomic_transfer_ptx.output_witness(0), // output state note + atomic_transfer_ptx.output_witness(1), // output funds note + ), + ), + ( + zone_a_start.fund_input_witness().nullifier(), + executor::prove_zone_fund_withdraw( + atomic_transfer_ptx.input_witness(2), // input fund note + atomic_transfer_ptx.output_witness(0), // output state note + &zone_a_end.state, + ), + ), + ( + zone_b_start.state_input_witness().nullifier(), + executor::prove_zone_stf( + zone_b_start.state.clone(), + vec![(signed_deposit, atomic_transfer_ptx.input_witness(0))], // deposit bound to input intent note + atomic_transfer_ptx.input_witness(3), // input state note + atomic_transfer_ptx.output_witness(2), // output state note + atomic_transfer_ptx.output_witness(3), // output funds note + ), + ), + ( + zone_b_start.fund_input_witness().nullifier(), + executor::prove_zone_fund_withdraw( + atomic_transfer_ptx.input_witness(4), // input fund note (input #1) + atomic_transfer_ptx.output_witness(2), // output state note (output #0) + &zone_b_end.state, + ), + ), + ]); + + let user_ptx_proof = + ledger::partial_tx::ProvedPartialTx::prove(&user_ptx, BTreeMap::new(), &[]) + .expect("user ptx failed to prove"); + assert!(user_ptx_proof.verify()); + + let note_commitments = vec![ + alice_intent_out.commit_note(), + zone_a_start.state_note.commit_note(), + zone_a_start.fund_note.commit_note(), + zone_b_start.state_note.commit_note(), + zone_b_start.fund_note.commit_note(), + ]; + + let atomic_transfer_proof = ledger::partial_tx::ProvedPartialTx::prove( + &atomic_transfer_ptx, + death_proofs, + ¬e_commitments, + ) + .expect("atomic transfer proof failed"); + + assert!(atomic_transfer_proof.verify()); + + let bundle = cl::Bundle { + partials: vec![user_ptx.commit(), atomic_transfer_ptx.commit()], + }; + + let bundle_witness = BundleWitness { + balance_blinding: cl::BalanceWitness( + user_ptx.balance_blinding().0 + atomic_transfer_ptx.balance_blinding().0, + ), + }; + + let bundle_proof = + ledger::bundle::ProvedBundle::prove(&bundle, &bundle_witness).expect("bundle proof failed"); + + assert!(bundle_proof.verify()); +} diff --git a/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs b/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs index 45afa0d..1483fba 100644 --- a/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs +++ b/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs @@ -1,65 +1,27 @@ use std::collections::BTreeMap; -use cl::{NoteWitness, NullifierNonce, NullifierSecret}; -use common::{BoundTx, StateWitness, Tx, ZoneMetadata, ZONE_CL_FUNDS_UNIT}; +use cl::{NoteWitness, NullifierSecret}; +use common::{new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; +use executor::ZoneNotes; use ledger::death_constraint::DeathProof; -use rand_core::CryptoRngCore; - -fn zone_fund_note(value: u64, zone_meta: ZoneMetadata) -> cl::NoteWitness { - cl::NoteWitness { - value, - unit: *common::ZONE_CL_FUNDS_UNIT, - death_constraint: zone_meta.funds_vk, - state: zone_meta.id(), - } -} - -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_deposit() { let mut rng = rand::thread_rng(); - let alice = 42; - let alice_sk = NullifierSecret::random(&mut rng); + let mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + let alice_cl_sk = NullifierSecret::random(&mut rng); - let init_state = StateWitness { - balances: BTreeMap::new(), - included_txs: vec![], - zone_metadata: executor::zone_metadata("ZONE"), - nonce: [0; 32], - }; - - let zone_state_in = cl::InputWitness::public(zone_state_utxo(&init_state, &mut rng)); + let zone_start = ZoneNotes::new_with_balances("ZONE", BTreeMap::new(), &mut rng); let deposit = common::Deposit { - to: alice, + to: alice_vk, amount: 78, }; - let end_state = init_state.clone().deposit(deposit).evolve_nonce(); + let zone_end = zone_start.clone().run([Tx::Deposit(deposit)]); - 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( - zone_fund_note(78, init_state.zone_metadata), - NullifierNonce::from_bytes(end_state.nonce), - ); let alice_deposit = cl::InputWitness::random( cl::OutputWitness::random( NoteWitness::stateless( @@ -67,28 +29,33 @@ fn test_deposit() { *ZONE_CL_FUNDS_UNIT, DeathProof::nop_constraint(), // alice should demand a tx inclusion proof for the deposit ), - alice_sk.commit(), + alice_cl_sk.commit(), &mut rng, ), - alice_sk, + alice_cl_sk, &mut rng, ); let deposit_ptx = cl::PartialTxWitness { - inputs: vec![zone_state_in, alice_deposit], - outputs: vec![zone_state_out, zone_fund_out], + inputs: vec![zone_start.state_input_witness(), alice_deposit], + outputs: vec![zone_end.state_note, zone_end.fund_note], }; + let signed_deposit = SignedBoundTx::sign( + BoundTx { + tx: Tx::Deposit(deposit), + bind: alice_deposit.note_commitment(), + }, + &mut alice, + ); + let death_proofs = BTreeMap::from_iter([ ( - zone_state_in.nullifier(), + zone_start.state_input_witness().nullifier(), executor::prove_zone_stf( - init_state.clone(), - vec![BoundTx { - tx: Tx::Deposit(deposit), - bind: deposit_ptx.input_witness(1), // bind it to the deposit note - }], - deposit_ptx.input_witness(0), // input state note (input #0) + zone_start.state.clone(), + vec![(signed_deposit, deposit_ptx.input_witness(1))], // bind it to the deposit note)], + deposit_ptx.input_witness(0), // input state note (input #0) deposit_ptx.output_witness(0), // output state note (output #0) deposit_ptx.output_witness(1), // output funds note (output #1) ), @@ -100,7 +67,7 @@ fn test_deposit() { ]); let note_commitments = vec![ - zone_state_in.note_commitment(), + zone_start.state_note.commit_note(), alice_deposit.note_commitment(), ]; @@ -110,14 +77,16 @@ fn test_deposit() { assert!(deposit_proof.verify()); - assert_eq!(deposit_proof.outputs[0].output, zone_state_out.commit()); assert_eq!( - zone_state_out.note.state, + deposit_proof.outputs[0].output, + zone_end.state_note.commit() + ); + assert_eq!( + zone_end.state_note.note.state, StateWitness { - balances: BTreeMap::from_iter([(alice, 78)]), + balances: BTreeMap::from_iter([(alice_vk, 78)]), included_txs: vec![Tx::Deposit(deposit)], - zone_metadata: init_state.zone_metadata, - nonce: init_state.evolve_nonce().nonce, + zone_metadata: zone_start.state.zone_metadata, } .commit() .0 diff --git a/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs index 75fc45c..ae189c4 100644 --- a/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs +++ b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs @@ -1,87 +1,37 @@ use std::collections::BTreeMap; -use cl::{NoteWitness, NullifierNonce, NullifierSecret}; -use common::{BoundTx, StateWitness, Tx, ZoneMetadata, ZONE_CL_FUNDS_UNIT}; +use cl::{NoteWitness, NullifierSecret}; +use common::{new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; +use executor::ZoneNotes; use ledger::death_constraint::DeathProof; -use rand_core::CryptoRngCore; - -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 mut alice = new_account(&mut rng); + let alice_vk = alice.verifying_key().to_bytes(); + let alice_cl_sk = NullifierSecret::random(&mut rng); - let init_state = StateWitness { - balances: BTreeMap::from_iter([(alice, 100)]), - included_txs: vec![], - zone_metadata: executor::zone_metadata("ZONE"), - nonce: [0; 32], - }; - - let zone_fund_in = - cl::InputWitness::public(zone_fund_utxo(100, init_state.zone_metadata, &mut rng)); - let zone_state_in = cl::InputWitness::public(zone_state_utxo(&init_state, &mut rng)); + let zone_start = + ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([(alice_vk, 100)]), &mut rng); let alice_intent = cl::InputWitness::random( cl::OutputWitness::random( NoteWitness::stateless(1, *ZONE_CL_FUNDS_UNIT, DeathProof::nop_constraint()), // TODO, intent should be in the death constraint - alice_sk.commit(), + alice_cl_sk.commit(), &mut rng, ), - alice_sk, + alice_cl_sk, &mut rng, ); let withdraw = common::Withdraw { - from: alice, + from: alice_vk, amount: 78, }; - let end_state = init_state.clone().withdraw(withdraw.clone()).evolve_nonce(); - - 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 - }, - NullifierNonce::from_bytes(end_state.nonce), - ); + let zone_end = zone_start.clone().run([Tx::Withdraw(withdraw)]); let alice_withdrawal = cl::OutputWitness::random( NoteWitness::stateless( @@ -89,35 +39,44 @@ fn test_withdrawal() { *ZONE_CL_FUNDS_UNIT, DeathProof::nop_constraint(), ), - alice_sk.commit(), + alice_cl_sk.commit(), &mut rng, ); let withdraw_ptx = cl::PartialTxWitness { - inputs: vec![zone_state_in, zone_fund_in, alice_intent], - outputs: vec![zone_state_out, zone_fund_out, alice_withdrawal], + inputs: vec![ + zone_start.state_input_witness(), + zone_start.fund_input_witness(), + alice_intent, + ], + outputs: vec![zone_end.state_note, zone_end.fund_note, alice_withdrawal], }; + let signed_withdraw = SignedBoundTx::sign( + BoundTx { + tx: Tx::Withdraw(withdraw), + bind: alice_intent.note_commitment(), + }, + &mut alice, + ); + let death_proofs = BTreeMap::from_iter([ ( - zone_state_in.nullifier(), + zone_start.state_input_witness().nullifier(), executor::prove_zone_stf( - init_state.clone(), - vec![BoundTx { - tx: Tx::Withdraw(withdraw.clone()), - bind: withdraw_ptx.input_witness(2), - }], + zone_start.state.clone(), + vec![(signed_withdraw, withdraw_ptx.input_witness(2))], withdraw_ptx.input_witness(0), // input state note (input #0) withdraw_ptx.output_witness(0), // output state note (output #0) withdraw_ptx.output_witness(1), // output funds note (output #1) ), ), ( - zone_fund_in.nullifier(), + zone_start.fund_input_witness().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) - &end_state, + &zone_end.state, ), ), ( @@ -127,8 +86,8 @@ fn test_withdrawal() { ]); let note_commitments = vec![ - zone_state_in.note_commitment(), - zone_fund_in.note_commitment(), + zone_start.state_note.commit_note(), + zone_start.fund_note.commit_note(), alice_intent.note_commitment(), ]; @@ -138,14 +97,16 @@ fn test_withdrawal() { assert!(withdraw_proof.verify()); - assert_eq!(withdraw_proof.outputs[0].output, zone_state_out.commit()); assert_eq!( - zone_state_out.note.state, + withdraw_proof.outputs[0].output, + zone_end.state_note.commit() + ); + assert_eq!( + zone_end.state_note.note.state, StateWitness { - balances: BTreeMap::from_iter([(alice, 22)]), + balances: BTreeMap::from_iter([(alice_vk, 22)]), included_txs: vec![Tx::Withdraw(withdraw)], - zone_metadata: init_state.zone_metadata, - nonce: init_state.evolve_nonce().nonce, + zone_metadata: zone_start.state.zone_metadata, } .commit() .0 diff --git a/goas/atomic_asset_transfer/proof_statements/src/user_note.rs b/goas/atomic_asset_transfer/proof_statements/src/user_note.rs index f54c0b4..dd73924 100644 --- a/goas/atomic_asset_transfer/proof_statements/src/user_note.rs +++ b/goas/atomic_asset_transfer/proof_statements/src/user_note.rs @@ -26,7 +26,7 @@ use ledger_proof_statements::death_constraint::DeathConstraintPublic; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct UserIntent { pub zone_a_meta: common::ZoneMetadata, pub zone_b_meta: common::ZoneMetadata, diff --git a/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs b/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs index 1b26c88..d7c03be 100644 --- a/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs +++ b/goas/atomic_asset_transfer/proof_statements/src/zone_state.rs @@ -1,10 +1,10 @@ -use common::{BoundTx, StateWitness}; +use common::{SignedBoundTx, StateWitness}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ZoneStatePrivate { pub state: StateWitness, - pub inputs: Vec, + pub inputs: Vec<(SignedBoundTx, cl::PartialTxInputWitness)>, pub zone_in: cl::PartialTxInputWitness, pub zone_out: cl::PartialTxOutputWitness, /// While the absence of birth constraints does not guarantee uniqueness of a note that can be used as 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 7886fbf..ff18581 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,5 @@ use cl::{ - note::NoteWitness, nullifier::NullifierNonce, output::OutputWitness, PartialTxInputWitness, + note::NoteWitness, output::OutputWitness, PtxRoot, }; @@ -8,26 +8,6 @@ use goas_proof_statements::zone_state::ZoneStatePrivate; use ledger_proof_statements::death_constraint::DeathConstraintPublic; use risc0_zkvm::guest::env; -fn withdraw( - state: StateWitness, - input_root: [u8; 32], - withdrawal: Withdraw, - bind: PartialTxInputWitness, -) -> StateWitness { - assert_eq!(bind.input_root(), input_root); - state.withdraw(withdrawal) -} - -fn deposit( - state: StateWitness, - input_root: [u8; 32], - deposit: Deposit, - bind: PartialTxInputWitness, -) -> StateWitness { - assert_eq!(bind.input_root(), input_root); - state.deposit(deposit) -} - fn validate_zone_transition( in_note: cl::PartialTxInputWitness, out_note: cl::PartialTxOutputWitness, @@ -59,7 +39,7 @@ fn validate_zone_transition( ); // the nonce is correctly evolved - assert_eq!(in_note.input.evolved_nonce(), out_note.output.nonce); + assert_eq!(in_note.input.evolved_nonce(b"STATE_NONCE"), out_note.output.nonce); // funds are still under control of the zone let expected_note_witness = NoteWitness::new( @@ -72,7 +52,7 @@ fn validate_zone_transition( out_funds.output, OutputWitness::public( expected_note_witness, - NullifierNonce::from_bytes(out_state.nonce) + in_note.input.evolved_nonce(b"FUND_NONCE") ) ); // funds belong to the same partial tx @@ -98,20 +78,18 @@ fn main() { let in_state_cm = state.commit(); - for input in inputs { - state = match input { - BoundTx { - tx: Tx::Withdraw(w), - bind, - } => withdraw(state, input_root, w, bind), - BoundTx { - tx: Tx::Deposit(d), - bind, - } => deposit(state, input_root, d, bind), - } + for (signed_bound_tx, ptx_input_witness) in inputs { + // verify the signature + let bound_tx = signed_bound_tx.verify_and_unwrap(); + + // ensure the note this tx is bound to is present in the ptx + assert_eq!(bound_tx.bind, ptx_input_witness.input.note_commitment()); + assert_eq!(ptx_input_witness.input_root(), input_root); + + // apply the ptx + state = state.apply(bound_tx.tx) } - let state = state.evolve_nonce(); validate_zone_transition(zone_in, zone_out, funds_out, in_state_cm, state); env::commit(&pub_inputs); diff --git a/goas/cl/cl/src/input.rs b/goas/cl/cl/src/input.rs index b1b9209..8fa32d2 100644 --- a/goas/cl/cl/src/input.rs +++ b/goas/cl/cl/src/input.rs @@ -52,21 +52,25 @@ impl InputWitness { } } - pub fn evolved_nonce(&self) -> NullifierNonce { - self.nonce.evolve(&self.nf_sk) + pub fn evolved_nonce(&self, domain: &[u8]) -> NullifierNonce { + self.nonce.evolve(domain, &self.nf_sk, &self.note) } - pub fn evolve_output(&self, balance_blinding: BalanceWitness) -> crate::OutputWitness { + pub fn evolve_output( + &self, + domain: &[u8], + balance_blinding: BalanceWitness, + ) -> crate::OutputWitness { crate::OutputWitness { note: self.note, balance_blinding, nf_pk: self.nf_sk.commit(), - nonce: self.evolved_nonce(), + nonce: self.evolved_nonce(domain), } } pub fn nullifier(&self) -> Nullifier { - Nullifier::new(self.nf_sk, self.nonce) + Nullifier::new(self.nf_sk, self.note_commitment()) } pub fn commit(&self) -> Input { diff --git a/goas/cl/cl/src/note.rs b/goas/cl/cl/src/note.rs index 18d0186..2e70d67 100644 --- a/goas/cl/cl/src/note.rs +++ b/goas/cl/cl/src/note.rs @@ -23,7 +23,7 @@ pub fn unit_point(unit: &str) -> Unit { } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct NoteCommitment([u8; 32]); +pub struct NoteCommitment(pub [u8; 32]); impl NoteCommitment { pub fn as_bytes(&self) -> &[u8; 32] { @@ -31,8 +31,6 @@ impl NoteCommitment { } } -// TODO: Rename Note to NoteWitness and NoteCommitment to Note - #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct NoteWitness { pub value: u64, diff --git a/goas/cl/cl/src/nullifier.rs b/goas/cl/cl/src/nullifier.rs index e8ba99f..ada9f81 100644 --- a/goas/cl/cl/src/nullifier.rs +++ b/goas/cl/cl/src/nullifier.rs @@ -9,6 +9,8 @@ use rand_core::RngCore; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use crate::{NoteCommitment, NoteWitness}; + // TODO: create a nullifier witness and use it throughout. // struct NullifierWitness { // nf_sk: NullifierSecret, @@ -90,11 +92,12 @@ impl NullifierNonce { Self(bytes) } - pub fn evolve(&self, nf_sk: &NullifierSecret) -> Self { + pub fn evolve(&self, domain: &[u8], nf_sk: &NullifierSecret, note: &NoteWitness) -> Self { let mut hasher = Sha256::new(); hasher.update(b"NOMOS_COIN_EVOLVE"); - hasher.update(&self.0); + hasher.update(domain); hasher.update(nf_sk.0); + hasher.update(note.commit(nf_sk.commit(), *self).0); let nonce_bytes: [u8; 32] = hasher.finalize().into(); Self(nonce_bytes) @@ -102,11 +105,11 @@ impl NullifierNonce { } impl Nullifier { - pub fn new(sk: NullifierSecret, nonce: NullifierNonce) -> Self { + pub fn new(sk: NullifierSecret, note_cm: NoteCommitment) -> Self { let mut hasher = Sha256::new(); hasher.update(b"NOMOS_CL_NULLIFIER"); hasher.update(sk.0); - hasher.update(nonce.0); + hasher.update(note_cm.0); let nf_bytes: [u8; 32] = hasher.finalize().into(); Self(nf_bytes) @@ -119,6 +122,8 @@ impl Nullifier { #[cfg(test)] mod test { + use crate::{note::unit_point, NoteWitness}; + use super::*; #[ignore = "nullifier test vectors not stable yet"] @@ -142,10 +147,31 @@ mod test { fn test_nullifier_same_sk_different_nonce() { let mut rng = rand::thread_rng(); let sk = NullifierSecret::random(&mut rng); + let note = NoteWitness::basic(1, unit_point("NMO")); + let nonce_1 = NullifierNonce::random(&mut rng); let nonce_2 = NullifierNonce::random(&mut rng); - let nf_1 = Nullifier::new(sk, nonce_1); - let nf_2 = Nullifier::new(sk, nonce_2); + let note_cm_1 = note.commit(sk.commit(), nonce_1); + let note_cm_2 = note.commit(sk.commit(), nonce_2); + + 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 note_1 = NoteWitness::basic(1, unit_point("NMO")); + let note_2 = NoteWitness::basic(1, unit_point("ETH")); + let nonce = NullifierNonce::random(&mut rng); + let note_cm_1 = note_1.commit(sk.commit(), nonce); + let note_cm_2 = note_2.commit(sk.commit(), nonce); + + let nf_1 = Nullifier::new(sk, note_cm_1); + let nf_2 = Nullifier::new(sk, note_cm_2); assert_ne!(nf_1, nf_2); } diff --git a/goas/cl/ledger/src/input.rs b/goas/cl/ledger/src/input.rs index 1c3b9b1..f1ea9ee 100644 --- a/goas/cl/ledger/src/input.rs +++ b/goas/cl/ledger/src/input.rs @@ -127,7 +127,7 @@ mod test { input: cl::Input { nullifier: cl::Nullifier::new( cl::NullifierSecret::random(&mut rng), - cl::NullifierNonce::random(&mut rng), + input.note_commitment(), ), ..expected_public_inputs.input },