From bde46cefe7ff88f5bf695b58c8f559e1188d39c9 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 11 Mar 2025 19:02:55 +0400 Subject: [PATCH] swap scenario --- emmarin/apps/swapvm/stf/host/src/lib.rs | 94 +++++++++++++++++- emmarin/apps/swapvm/stf/host/tests/swap.rs | 96 ++++++++++++++----- .../apps/swapvm/stf/methods/guest/src/main.rs | 33 ++++--- 3 files changed, 184 insertions(+), 39 deletions(-) diff --git a/emmarin/apps/swapvm/stf/host/src/lib.rs b/emmarin/apps/swapvm/stf/host/src/lib.rs index 6beff11..ab2cf8a 100644 --- a/emmarin/apps/swapvm/stf/host/src/lib.rs +++ b/emmarin/apps/swapvm/stf/host/src/lib.rs @@ -1,10 +1,98 @@ -use app::{StateUpdate, ZoneOp}; -use cl::crust::BundleWitness; -use cl::mantle::{ledger::Ledger, zone::ZoneData}; +use std::collections::BTreeMap; + +use app::{swap_goal_unit, StateUpdate, ZoneData, ZoneOp}; +use cl::crust::{BundleWitness, InputWitness, NoteCommitment, Nullifier, Unit}; +use cl::ds::mmr::{MMRProof, MMR}; +use cl::mantle::ledger::Ledger; +use cl::mantle::ledger::LedgerState; use ledger_proof_statements::ledger::SyncLog; use methods::{STF_ELF, STF_ID}; use risc0_zkvm::{ExecutorEnv, Prover, Receipt, Result}; +#[derive(Debug)] +struct FundNote { + note: InputWitness, + mmr: MMR, + path: MMRProof, +} + +#[derive(Debug, Default)] +pub struct ExecutorState { + pub ledger: LedgerState, + pub swapvm: ZoneData, + pub fund_notes: BTreeMap, +} + +impl ExecutorState { + pub fn observe_cms(&mut self, cms: impl IntoIterator) { + for cm in cms { + self.ledger.add_commitment(&cm); + + // update merkle proofs for each fund note. + for (_, fund_note) in self.fund_notes.iter_mut() { + let folds = fund_note.mmr.folds(&cm.0); + fund_note + .path + .update(&fund_note.note.note_commitment().0, folds); + } + } + } + + pub fn observe_nfs(&mut self, nfs: Vec) { + self.ledger.add_nullifiers(nfs); + } + + // pub fn bundle_tx(&mut self, proved_tx: ProvedTx) -> StfPrivate { + // let tx = proved_tx.public(); + + // if tx.balance.unit_balance(swap_goal_unit()).is_neg() { + // // this is a SWAP + // let (swap_cm, swap_args_bytes) = &tx.outputs[0]; + // let swap_args: SwapArgs = cl::deserialize(&swap_args_bytes); + + // // verify the user proved the correct swap goal note + // assert_eq!( + // swap_cm, + // &app::swap_goal_note(swap_args.nonce).note_commitment() + // ); + + // // assume there are only the goal unit and tokenIn units at play + // assert_eq!(tx.balance.balances.len(), 2); + + // let balance_in = tx + // .balance + // .balances + // .iter() + // .find(|bal| bal.unit != swap_goal_unit()) + // .unwrap(); + + // let token_in = balance_in.unit; + // assert_eq!(balance_in.neg, 0); + // assert!(balance_in.pos > 0); + + // let amount_in = balance_in.pos; + // let amount_out = self + // .swapvm + // .amount_out(token_in, swap_args.output.unit, amount_in) + // .unwrap(); + + // // ensure we can satisfy the limit order + // assert!(amount_out > swap_args.limit); + + // // now build the balancing tx + + // let balancing_tx = TxWitness::default() + // .add_input(self.fund_notes[token_in], self.fund_notes[token_in].1) + // .add_input(self.fund_notes[token_out]); + // } + // } + + pub fn set_fund_note(&mut self, note: InputWitness, mmr: MMR, path: MMRProof) { + self.fund_notes + .insert(note.unit_witness.unit(), FundNote { note, mmr, path }); + } +} + pub struct StfPrivate { pub zone_data: ZoneData, pub old_ledger: Ledger, diff --git a/emmarin/apps/swapvm/stf/host/tests/swap.rs b/emmarin/apps/swapvm/stf/host/tests/swap.rs index cc92f9c..de5650a 100644 --- a/emmarin/apps/swapvm/stf/host/tests/swap.rs +++ b/emmarin/apps/swapvm/stf/host/tests/swap.rs @@ -1,8 +1,8 @@ -use app::{AddLiquidity, ZoneData, ZONE_ID}; -use cl::{ - crust::{InputWitness, Nonce, NullifierSecret, TxWitness, UnitWitness}, - mantle::ledger::LedgerState, -}; +use app::{AddLiquidity, ZONE_ID}; +use cl::crust::{InputWitness, Nonce, NullifierSecret, TxWitness, UnitWitness}; +use host::ExecutorState; +use ledger::tx::ProvedTx; +use rand::RngCore; fn nmo() -> UnitWitness { UnitWitness::nop(b"NMO") @@ -11,10 +11,59 @@ fn mem() -> UnitWitness { UnitWitness::nop(b"MEM") } +fn setup_executor(mut rng: impl RngCore) -> ExecutorState { + let mut exec_state = ExecutorState::default(); + + let nmo_fund = InputWitness { + state: [0u8; 32], + value: 1348, + unit_witness: nmo(), + nonce: Nonce::random(&mut rng), + zone_id: ZONE_ID, + nf_sk: NullifierSecret::zero(), + }; + + let (mmr, mmr_proof) = exec_state + .ledger + .add_commitment(&nmo_fund.note_commitment()); + exec_state.set_fund_note(nmo_fund, mmr, mmr_proof); + + let mem_fund = InputWitness { + state: [0u8; 32], + value: 14102, + unit_witness: mem(), + nonce: Nonce::random(&mut rng), + zone_id: ZONE_ID, + nf_sk: NullifierSecret::zero(), + }; + let (mmr, mmr_proof) = exec_state + .ledger + .add_commitment(&mem_fund.note_commitment()); + exec_state.set_fund_note(mem_fund, mmr, mmr_proof); + + // HACK: we don't currently support liquidity notes, we directly hard code the corresponding liquidity + // in the swapvm instead of minting pool LP tokens + exec_state.swapvm.add_liquidity(&AddLiquidity::new( + nmo().unit(), + nmo_fund.value, + mem().unit(), + mem_fund.value, + NullifierSecret::random(&mut rng).commit(), + Nonce::random(&mut rng), + )); + + exec_state +} + #[test] fn simple_swap() { let mut rng = rand::thread_rng(); + // ---- setup scenario ---- + let mut exec_state = setup_executor(&mut rng); + + // setup fund notes + let alice_sk = NullifierSecret::random(&mut rng); let alice_in = InputWitness { @@ -26,13 +75,18 @@ fn simple_swap() { nf_sk: alice_sk, }; - let mut ledger = LedgerState::default(); + // alice's note lands in the ledger through some other executor + let mut other_exec_ledger = exec_state.ledger.clone(); + let alice_in_proof = other_exec_ledger.add_commitment(&alice_in.note_commitment()); - // alice's input note is already in the ledger - let alice_in_proof = ledger.add_commitment(&alice_in.note_commitment()); + // executor becomes aware of the commitment through observing a zone update + exec_state.observe_cms([alice_in.note_commitment()]); + + // ----- end setup ---- + // Alice now has a valid 10 NMO note, she wants to swap it for 90 MEM + // ---- begin swap ---- let swap_goal_nonce = Nonce::random(&mut rng); - let swap_tx = TxWitness::default() .add_input(alice_in, alice_in_proof) .add_output( @@ -44,30 +98,26 @@ fn simple_swap() { }, ); - let swap_tx_proof = ledger::tx::ProvedTx::prove(swap_tx, vec![], vec![]).unwrap(); + let swap_tx_proof = ProvedTx::prove(swap_tx, vec![], vec![]).unwrap(); + // // alice ---- (swap_tx, swap_tx_proof) ---> executor - - let mut swapvm_state = ZoneData::new(); - - swapvm_state.add_liquidity(&AddLiquidity::new( - nmo().unit(), - 1348, - mem().unit(), - 14102, - NullifierSecret::random(&mut rng).commit(), - Nonce::random(&mut rng), - )); + // + // alice sends the tx to an executor // ensure the pair price is above the minimum realized price (90 out / 10 in = 9.0) assert_eq!( - swapvm_state.pair_price(nmo().unit(), mem().unit()).unwrap(), + exec_state + .swapvm + .pair_price(nmo().unit(), mem().unit()) + .unwrap(), 9.0 ); // ensure that the realized output is above the limit order assert!( - swapvm_state + exec_state + .swapvm .amount_out(nmo().unit(), mem().unit(), 10) .unwrap() >= 90 diff --git a/emmarin/apps/swapvm/stf/methods/guest/src/main.rs b/emmarin/apps/swapvm/stf/methods/guest/src/main.rs index ed59da4..41a5a80 100644 --- a/emmarin/apps/swapvm/stf/methods/guest/src/main.rs +++ b/emmarin/apps/swapvm/stf/methods/guest/src/main.rs @@ -41,21 +41,28 @@ fn main() { continue }; - zone_data.validate_no_pools(zone_update); + if zone_data.validate_no_pools(zone_update) { + // This tx does not touch pool notes, we can allow zone ops. - if tx.balance.unit_balance(app::swap_goal_unit().unit()).is_neg() { - // This TX encodes a SWAP request. - // as a simplifying assumption, we will assume that the SWAP goal note is the only output - assert_eq!(zone_update.outputs.len(), 1); - let (swap_goal_cm, swap_args_bytes) = &zone_update.outputs[0]; - let swap_args: SwapArgs = cl::deserialize(&swap_args_bytes); + // is it a SWAP? + if tx.balance.unit_balance(app::swap_goal_unit().unit()).is_neg() { + // This TX encodes a SWAP request. + // as a simplifying assumption, we will assume that the SWAP goal note is the only output + assert_eq!(zone_update.outputs.len(), 1); + let (swap_goal_cm, swap_args_bytes) = &zone_update.outputs[0]; + let swap_args: SwapArgs = cl::deserialize(&swap_args_bytes); - // ensure the witness corresponds to the swap goal cm - assert_eq!( - swap_goal_cm, - &app::swap_goal_note(swap_args.nonce).note_commitment() - ); - panic!("zone_data.swap()"); + // ensure the witness corresponds to the swap goal cm + assert_eq!( + swap_goal_cm, + &app::swap_goal_note(swap_args.nonce).note_commitment() + ); + panic!("zone_data.swap()"); + } + // otherwise it's a normal ledger Tx + } else { + // This tx does touch pool notes. therefore we must ensure the changes reflect the zone balances + panic!(); } } }