update stf proof to read Swap Args from output data

This commit is contained in:
David Rusu 2025-03-10 14:37:28 +04:00
parent 545d5b822f
commit 9a131c6718
9 changed files with 166 additions and 131 deletions

View File

@ -1,6 +1,7 @@
use cl::{
crust::{
balance::{UnitWitness, NOP_COVENANT},
tx::LedgerUpdate,
InputWitness, Nonce, Nullifier, NullifierCommitment, NullifierSecret, OutputWitness, Tx,
Unit,
},
@ -50,6 +51,8 @@ pub struct SwapArgs {
pub output: SwapOutput,
// minimum value of the output note
pub limit: u64,
// the nonce used in the swap goal note
pub nonce: Nonce,
}
impl SwapArgs {
@ -66,12 +69,12 @@ impl SwapArgs {
}
}
pub fn swap_goal_note(rng: impl RngCore) -> OutputWitness {
pub fn swap_goal_note(nonce: Nonce) -> OutputWitness {
OutputWitness {
state: [0u8; 32],
value: 1,
unit: swap_goal_unit().unit(),
nonce: Nonce::random(rng),
nonce,
zone_id: ZONE_ID,
nf_pk: NullifierSecret::zero().commit(),
}
@ -310,25 +313,38 @@ impl ZoneData {
}
/// Check no pool notes are used in this tx
pub fn validate_no_pools(&self, tx: &Tx) -> bool {
let Some(zone_update) = tx.updates.get(&self.zone_id) else {
// this tx is not involving this zone, therefore it is
// guaranteed to not consume pool notes
return true;
};
pub fn validate_no_pools(&self, zone_update: &LedgerUpdate) -> bool {
self.nfs.iter().all(|nf| !zone_update.has_input(nf))
}
pub fn validate_op(&self, op: &ZoneOp) -> bool {
match op {
ZoneOp::Swap(swap) => self.check_swap(swap),
ZoneOp::AddLiquidity { tx, .. } => self.validate_no_pools(tx),
ZoneOp::RemoveLiquidity { tx, .. } => self.validate_no_pools(tx), // should we check shares exist?
ZoneOp::AddLiquidity { tx, .. } => {
let Some(zone_update) = tx.updates.get(&self.zone_id) else {
// this tx is not involving this zone, therefore it is
// guaranteed to not consume pool notes
return true;
};
self.validate_no_pools(zone_update)
}
ZoneOp::RemoveLiquidity { tx, .. } => {
let Some(zone_update) = tx.updates.get(&self.zone_id) else {
// this tx is not involving this zone, therefore it is
// guaranteed to not consume pool notes
return true;
};
self.validate_no_pools(zone_update) // should we check shares exist?
}
ZoneOp::Ledger(tx) => {
let Some(zone_update) = tx.updates.get(&self.zone_id) else {
// this tx is not involving this zone, therefore it is
// guaranteed to not consume pool notes
return true;
};
// Just a ledger tx that does not directly interact with the zone,
// just validate it's not using pool notes
self.validate_no_pools(tx)
self.validate_no_pools(zone_update)
}
}
}
@ -433,26 +449,19 @@ impl ZoneData {
pub fn process_op(&mut self, op: &ZoneOp) {
match op {
ZoneOp::Swap(swap) => self.swap(swap),
ZoneOp::AddLiquidity {
tx, add_liquidity, ..
} => {
ZoneOp::AddLiquidity { add_liquidity, .. } => {
self.add_liquidity(add_liquidity);
assert!(self.validate_no_pools(tx));
// TODo: check proof
}
ZoneOp::RemoveLiquidity {
tx,
remove_liquidity,
..
remove_liquidity, ..
} => {
self.remove_liquidity(remove_liquidity);
assert!(self.validate_no_pools(tx));
// TODO: check proof
}
ZoneOp::Ledger(tx) => {
ZoneOp::Ledger(_) => {
// Just a ledger tx that does not directly interact with the zone,
// just validate it's not using pool notes
self.validate_no_pools(tx);
}
}
}

View File

@ -1,8 +1,5 @@
use app::{AddLiquidity, SwapArgs, SwapOutput, ZoneData, ZONE_ID};
use cl::{
crust::{InputWitness, Nonce, NullifierSecret, OutputWitness, TxWitness, UnitWitness},
mantle::ledger::LedgerState,
};
use app::{AddLiquidity, ZoneData};
use cl::crust::{Nonce, NullifierSecret, UnitWitness};
fn nmo() -> UnitWitness {
UnitWitness::nop(b"NMO")
@ -56,47 +53,3 @@ fn pair_price() {
Some(39) // 11 MEM slippage
);
}
#[test]
fn simple_swap() {
let mut rng = rand::thread_rng();
let alice_sk = NullifierSecret::random(&mut rng);
let alice_in = InputWitness {
state: [0u8; 32],
value: 10,
unit_witness: nmo(),
nonce: Nonce::random(&mut rng),
zone_id: ZONE_ID,
nf_sk: alice_sk,
};
let alice_out = OutputWitness {
state: [0u8; 32],
value: 100,
unit: mem().unit(),
nonce: Nonce::random(&mut rng),
zone_id: ZONE_ID,
nf_pk: alice_sk.commit(),
};
let mut ledger = LedgerState::default();
// alice's input note is already in the ledger
let alice_in_proof = ledger.add_commitment(&alice_in.note_commitment());
let swap_tx = TxWitness::default()
.add_input(alice_in, alice_in_proof)
.add_output(alice_out, b"")
.add_output(
app::swap_goal_note(&mut rng),
SwapArgs {
output: SwapOutput::basic(mem().unit(), ZONE_ID, alice_sk.commit(), &mut rng),
limit: 90,
},
);
panic!()
// alice ---- swap_tx ---> executor
}

View File

@ -4,10 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8"
methods = { path = "../methods" }
risc0-zkvm = { version = "1.2.0" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde = "1.0"
ledger_proof_statements = { path = "../../../../cl/ledger_proof_statements" }
app = { path = "../../app" }
cl = { path = "../../../../cl/cl" }
cl = { path = "../../../../cl/cl" }
ledger = { path = "../../../../cl/ledger" }

View File

@ -0,0 +1,77 @@
use app::{AddLiquidity, ZoneData, ZONE_ID};
use cl::{
crust::{InputWitness, Nonce, NullifierSecret, TxWitness, UnitWitness},
mantle::ledger::LedgerState,
};
fn nmo() -> UnitWitness {
UnitWitness::nop(b"NMO")
}
fn mem() -> UnitWitness {
UnitWitness::nop(b"MEM")
}
#[test]
fn simple_swap() {
let mut rng = rand::thread_rng();
let alice_sk = NullifierSecret::random(&mut rng);
let alice_in = InputWitness {
state: [0u8; 32],
value: 10,
unit_witness: nmo(),
nonce: Nonce::random(&mut rng),
zone_id: ZONE_ID,
nf_sk: alice_sk,
};
let mut ledger = LedgerState::default();
// alice's input note is already in the ledger
let alice_in_proof = ledger.add_commitment(&alice_in.note_commitment());
let swap_goal_nonce = Nonce::random(&mut rng);
let swap_tx = TxWitness::default()
.add_input(alice_in, alice_in_proof)
.add_output(
app::swap_goal_note(swap_goal_nonce),
app::SwapArgs {
output: app::SwapOutput::basic(mem().unit(), ZONE_ID, alice_sk.commit(), &mut rng),
limit: 90,
nonce: swap_goal_nonce,
},
);
let swap_tx_proof = ledger::tx::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),
));
// 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(),
9.0
);
// ensure that the realized output is above the limit order
assert!(
swapvm_state
.amount_out(nmo().unit(), mem().unit(), 10)
.unwrap()
>= 90
);
panic!();
}

View File

@ -1,78 +1,75 @@
use app::{StateUpdate, ZoneData, ZoneOp};
use app::{StateUpdate, ZoneData, ZoneOp, SwapArgs};
use cl::{
crust::Tx,
mantle::{ledger::Ledger, zone::ZoneState},
crust::{BundleWitness, TxRoot},
mantle::{
ledger::{Ledger, LedgerWitness},
zone::ZoneState,
},
};
use ledger_proof_statements::{
ledger::{LedgerProofPublic, SyncLog},
stf::StfPublic,
};
use risc0_zkvm::guest::env;
fn main() {
let mut zone_data: ZoneData = env::read();
let old_ledger: Ledger = env::read();
let ledger: Ledger = env::read();
let sync_logs: Vec<SyncLog> = env::read();
let mut ledger_witness: LedgerWitness = env::read();
let stf: [u8; 32] = env::read();
let new_ledger: Ledger = env::read();
let bundles: Vec<BundleWitness> = env::read();
let ops: Vec<ZoneOp> = env::read();
let update_tx: StateUpdate = env::read();
let zone_id = zone_data.zone_id;
let old_zone_data = zone_data.commit();
let old_state = ZoneState {
ledger: ledger_witness.commit(),
zone_data: zone_data.commit(),
stf,
};
for op in &ops {
zone_data.process_op(op);
}
let txs: Vec<&Tx> = ops
.iter()
.filter_map(|op| match op {
ZoneOp::Swap(_) => None,
ZoneOp::AddLiquidity { tx, .. } => Some(tx),
ZoneOp::RemoveLiquidity { tx, .. } => Some(tx),
ZoneOp::Ledger(tx) => Some(tx),
})
.chain(std::iter::once(&update_tx.tx))
.collect();
for bundle in bundles {
ledger_witness.add_bundle(bundle.root());
let outputs = txs
.iter()
.flat_map(|tx| tx.updates.iter().filter(|u| u.zone_id == zone_id))
.flat_map(|u| u.outputs.iter())
.copied()
.collect();
// TODO: inputs missings from ledger proof public
let _inputs: Vec<_> = txs
.iter()
.flat_map(|tx| tx.updates.iter().filter(|u| u.zone_id == zone_id))
.flat_map(|u| u.inputs.iter())
.copied()
.collect();
for tx in bundle.txs {
let Some(zone_update) = tx.updates.get(&zone_id) else {
// this tx does not concern this zone, ignore it.
continue
};
let ledger_public = LedgerProofPublic {
old_ledger,
ledger,
id: zone_id,
sync_logs,
outputs,
};
zone_data.validate_no_pools(zone_update);
env::verify(
ledger_validity_proof::LEDGER_ID,
&risc0_zkvm::serde::to_vec(&ledger_public).unwrap(),
)
.unwrap();
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 we had processed all the ops
assert!(ops.is_empty());
// ensure that we've seen every bundle in this ledger update
assert_eq!(ledger_witness.bundles.commit(), new_ledger.bundles_root);
let public = StfPublic {
old: ZoneState {
ledger: old_ledger,
zone_data: old_zone_data,
stf,
},
old: old_state,
new: ZoneState {
ledger,
ledger: new_ledger,
zone_data: zone_data.update_and_commit(&update_tx),
stf,
},

View File

@ -9,7 +9,7 @@ rand_core = "0.6.0"
hex = "0.4.3"
risc0-zkvm = "1.2"
itertools = "0.14"
bincode = { version = "2", features = ["serde"] }
bincode = "1"
[dev-dependencies]
rand = "0.8.5"

View File

@ -15,7 +15,7 @@ use crate::{
};
/// An identifier of a transaction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct TxRoot(pub [u8; 32]);
/// An identifier of a bundle

View File

@ -15,12 +15,9 @@ pub fn hash(data: &[u8]) -> [u8; 32] {
// TODO: spec serializiation
pub fn serialize(data: impl Serialize) -> Vec<u8> {
bincode::serde::encode_to_vec(data, bincode::config::standard()).unwrap()
bincode::serialize(&data).unwrap()
}
pub fn deserialize<T: DeserializeOwned>(bytes: &[u8]) -> T {
let (value, bytes_read) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())
.expect("failed to deserialize");
assert_eq!(bytes_read, bytes.len());
value
bincode::deserialize(bytes).unwrap()
}

View File

@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Ledger {
cm_root: [u8; 32],
nf_root: [u8; 32],
bundles_root: [u8; 32],
pub cm_root: [u8; 32],
pub nf_root: [u8; 32],
pub bundles_root: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]