From d73508a43cdd94892eb6a7b0b0cf1a20fef63004 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Sat, 10 Aug 2024 21:09:36 +0400 Subject: [PATCH] goas: atomic transfer scenario --- goas/atomic_asset_transfer/common/src/lib.rs | 18 +- .../atomic_asset_transfer/executor/src/lib.rs | 58 +++- .../executor/tests/atomic_transfer.rs | 257 ++++++++++++++++++ .../executor/tests/deposit_ptx.rs | 5 +- .../executor/tests/withdraw_ptx.rs | 5 +- .../proof_statements/src/user_note.rs | 2 +- .../risc0_proofs/zone_state/src/main.rs | 36 +-- 7 files changed, 339 insertions(+), 42 deletions(-) create mode 100644 goas/atomic_asset_transfer/executor/tests/atomic_transfer.rs diff --git a/goas/atomic_asset_transfer/common/src/lib.rs b/goas/atomic_asset_transfer/common/src/lib.rs index c76a3c6..7786454 100644 --- a/goas/atomic_asset_transfer/common/src/lib.rs +++ b/goas/atomic_asset_transfer/common/src/lib.rs @@ -57,7 +57,18 @@ impl StateWitness { } } - 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 +76,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 +87,6 @@ impl StateWitness { .checked_add(amount) .expect("overflow in account balance"); - self.included_txs.push(Tx::Deposit(d)); self } diff --git a/goas/atomic_asset_transfer/executor/src/lib.rs b/goas/atomic_asset_transfer/executor/src/lib.rs index 7565f79..3a13794 100644 --- a/goas/atomic_asset_transfer/executor/src/lib.rs +++ b/goas/atomic_asset_transfer/executor/src/lib.rs @@ -1,5 +1,16 @@ -use common::{BoundTx, StateWitness, ZoneMetadata}; -use goas_proof_statements::{zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate}; +use cl::{PartialTxInputWitness, PartialTxOutputWitness}; +use common::{BoundTx, IncludedTxWitness, StateRoots, StateWitness, ZoneMetadata}; +use goas_proof_statements::{ + user_note::{UserAtomicTransfer, UserIntent}, + zone_funds::SpendFundsPrivate, + zone_state::ZoneStatePrivate, +}; + +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) @@ -82,3 +93,46 @@ pub fn prove_zone_fund_withdraw( let receipt = prove_info.receipt; ledger::DeathProof::from_risc0(goas_risc0_proofs::SPEND_ZONE_FUNDS_ID, receipt) } + +pub fn prove_user_atomic_transfer( + user_note: PartialTxInputWitness, + user_intent: UserIntent, + zone_a: PartialTxOutputWitness, + zone_b: PartialTxOutputWitness, + zone_a_roots: StateRoots, + zone_b_roots: StateRoots, + withdraw_tx: IncludedTxWitness, + deposit_tx: IncludedTxWitness, +) -> ledger::DeathProof { + let private_inputs = UserAtomicTransfer { + user_note, + user_intent, + zone_a, + zone_b, + zone_a_roots, + zone_b_roots, + withdraw_tx, + deposit_tx, + }; + + 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::USER_ATOMIC_TRANSFER_ELF, &opts) + .unwrap(); + println!( + "STARK 'user atomic transfer' prover time: {:.2?}", + start_t.elapsed() + ); + 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..4866dbd --- /dev/null +++ b/goas/atomic_asset_transfer/executor/tests/atomic_transfer.rs @@ -0,0 +1,257 @@ +use std::collections::BTreeMap; + +use cl::{BundleWitness, NoteWitness, NullifierNonce}; +use common::{BoundTx, Deposit, StateWitness, Tx, Withdraw, ZoneMetadata}; +use goas_proof_statements::user_note::UserIntent; +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), + ) +} + +#[derive(Debug, Clone)] +struct ZoneNotes { + state: StateWitness, + state_note: cl::OutputWitness, + fund_note: cl::OutputWitness, +} + +impl ZoneNotes { + fn new_with_balances( + zone_name: &str, + balances: BTreeMap, + mut rng: impl CryptoRngCore, + ) -> Self { + let state = StateWitness { + balances, + included_txs: vec![], + zone_metadata: executor::zone_metadata(zone_name), + nonce: [0; 32], + }; + 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, + } + } + + fn state_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.state_note) + } + + fn fund_input_witness(&self) -> cl::InputWitness { + cl::InputWitness::public(self.fund_note) + } + + fn run(mut self, txs: Vec) -> Self { + for tx in txs { + self.state = self.state.apply(tx); + } + self.state = self.state.evolve_nonce(); + + 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(), + ); + + let fund_in = self.fund_input_witness(); + self.fund_note = cl::OutputWitness::public( + cl::NoteWitness { + value: self.state.total_balance(), + ..fund_in.note + }, + NullifierNonce::from_bytes(self.state.nonce), + ); + self + } +} + +#[test] +fn test_atomic_transfer() { + let mut rng = rand::thread_rng(); + + let alice = 42; + + let zone_a_start = + ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice, 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, + amount: 75, + }, + deposit: Deposit { + to: alice, + 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(vec![Tx::Withdraw(alice_intent.withdraw)]); + + let zone_b_end = zone_b_start + .clone() + .run(vec![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 death_proofs = BTreeMap::from_iter([ + ( + alice_intent_in.nullifier(), + executor::prove_user_atomic_transfer( + atomic_transfer_ptx.input_witness(0), + alice_intent, + atomic_transfer_ptx.output_witness(0), + atomic_transfer_ptx.output_witness(2), + zone_a_end.state.state_roots(), + zone_b_end.state.state_roots(), + zone_a_end.state.included_tx_witness(0), + 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![BoundTx { + tx: Tx::Withdraw(alice_intent.withdraw), + bind: atomic_transfer_ptx.input_witness(0), // 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![BoundTx { + tx: Tx::Deposit(alice_intent.deposit), + bind: atomic_transfer_ptx.input_witness(0), // 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..fedab1f 100644 --- a/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs +++ b/goas/atomic_asset_transfer/executor/tests/deposit_ptx.rs @@ -47,7 +47,10 @@ fn test_deposit() { amount: 78, }; - let end_state = init_state.clone().deposit(deposit).evolve_nonce(); + let end_state = init_state + .clone() + .apply(Tx::Deposit(deposit)) + .evolve_nonce(); let zone_state_out = cl::OutputWitness::public( cl::NoteWitness { diff --git a/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs index 75fc45c..155b5c4 100644 --- a/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs +++ b/goas/atomic_asset_transfer/executor/tests/withdraw_ptx.rs @@ -66,7 +66,10 @@ fn test_withdrawal() { amount: 78, }; - let end_state = init_state.clone().withdraw(withdraw.clone()).evolve_nonce(); + let end_state = init_state + .clone() + .apply(Tx::Withdraw(withdraw.clone())) + .evolve_nonce(); let zone_state_out = cl::OutputWitness::public( cl::NoteWitness { 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/risc0_proofs/zone_state/src/main.rs b/goas/atomic_asset_transfer/risc0_proofs/zone_state/src/main.rs index 7886fbf..1638536 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, nullifier::NullifierNonce, 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, @@ -98,17 +78,9 @@ 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 BoundTx { tx, bind } in inputs { + assert_eq!(bind.input_root(), input_root); + state = state.apply(tx) } let state = state.evolve_nonce();