From 7e67014042cfcc6699480ef600d62b54b644733b Mon Sep 17 00:00:00 2001 From: Giacomo Pasini <21265557+zeegomo@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:46:17 +0200 Subject: [PATCH] Deposit (#8) * Add native zone deposits * check note unit and death constraints * fix deposit logic * fix ptx merkle root derivation * restrict ptx inputs/outputs * re-org directories --- goas/cl/cl/src/balance.rs | 15 +- goas/cl/cl/src/note.rs | 10 +- goas/cl/cl/src/nullifier.rs | 4 + goas/cl/cl/src/partial_tx.rs | 4 +- goas/zone/Cargo.toml | 2 +- goas/zone/common/Cargo.toml | 4 +- goas/zone/common/src/lib.rs | 60 +++++--- goas/zone/{host => executor}/Cargo.toml | 2 +- .../{host => executor}/src/bin/groth16.rs | 0 goas/zone/{host => executor}/src/main.rs | 4 +- goas/zone/methods/Cargo.toml | 10 -- goas/zone/methods/build.rs | 3 - goas/zone/methods/guest/src/main.rs | 40 ----- goas/zone/methods/src/lib.rs | 1 - goas/zone/proof_statements/src/zone_funds.rs | 1 - goas/zone/risc0_proofs/Cargo.toml | 2 +- .../risc0_proofs/spend_zone_funds/src/main.rs | 10 +- .../zone_state}/Cargo.toml | 6 +- goas/zone/risc0_proofs/zone_state/src/main.rs | 140 ++++++++++++++++++ 19 files changed, 216 insertions(+), 102 deletions(-) rename goas/zone/{host => executor}/Cargo.toml (84%) rename goas/zone/{host => executor}/src/bin/groth16.rs (100%) rename goas/zone/{host => executor}/src/main.rs (93%) delete mode 100644 goas/zone/methods/Cargo.toml delete mode 100644 goas/zone/methods/build.rs delete mode 100644 goas/zone/methods/guest/src/main.rs delete mode 100644 goas/zone/methods/src/lib.rs rename goas/zone/{methods/guest => risc0_proofs/zone_state}/Cargo.toml (85%) create mode 100644 goas/zone/risc0_proofs/zone_state/src/main.rs diff --git a/goas/cl/cl/src/balance.rs b/goas/cl/cl/src/balance.rs index b49f18b..416a103 100644 --- a/goas/cl/cl/src/balance.rs +++ b/goas/cl/cl/src/balance.rs @@ -13,17 +13,20 @@ lazy_static! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Balance(pub RistrettoPoint); +pub type Value = u64; +pub type Unit = RistrettoPoint; + #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct BalanceWitness(pub Scalar); impl Balance { /// A commitment to zero, blinded by the provided balance witness pub fn zero(blinding: BalanceWitness) -> Self { - // Since, balance commitments are `value * UnitPoint + blinding * H`, when value=0, the commmitment is unitless. - // So we use the generator point as a stand in for the unit point. - // - // TAI: we can optimize this further from `0*G + r*H` to just `r*H` to save a point scalar mult + point addition. - let unit = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; + // Since, balance commitments are `value * UnitPoint + blinding * H`, when value=0, the commmitment is unitless. + // So we use the generator point as a stand in for the unit point. + // + // TAI: we can optimize this further from `0*G + r*H` to just `r*H` to save a point scalar mult + point addition. + let unit = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; Self(balance(0, unit, blinding.0)) } @@ -46,7 +49,7 @@ impl BalanceWitness { } } -pub fn balance(value: u64, unit: RistrettoPoint, blinding: Scalar) -> RistrettoPoint { +pub fn balance(value: u64, unit: Unit, blinding: Scalar) -> Unit { let value_scalar = Scalar::from(value); // can vartime leak the number of cycles through the stark proof? RistrettoPoint::vartime_multiscalar_mul( diff --git a/goas/cl/cl/src/note.rs b/goas/cl/cl/src/note.rs index 10a773e..644744c 100644 --- a/goas/cl/cl/src/note.rs +++ b/goas/cl/cl/src/note.rs @@ -1,8 +1,10 @@ -use curve25519_dalek::RistrettoPoint; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::nullifier::{NullifierCommitment, NullifierNonce}; +use crate::{ + balance::Unit, + nullifier::{NullifierCommitment, NullifierNonce}, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct DeathCommitment(pub [u8; 32]); @@ -16,7 +18,7 @@ pub fn death_commitment(death_constraint: &[u8]) -> DeathCommitment { DeathCommitment(death_cm) } -pub fn unit_point(unit: &str) -> RistrettoPoint { +pub fn unit_point(unit: &str) -> Unit { crate::crypto::hash_to_curve(unit.as_bytes()) } @@ -34,7 +36,7 @@ impl NoteCommitment { #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct NoteWitness { pub value: u64, - pub unit: RistrettoPoint, + pub unit: Unit, pub death_constraint: [u8; 32], // death constraint verification key pub state: [u8; 32], } diff --git a/goas/cl/cl/src/nullifier.rs b/goas/cl/cl/src/nullifier.rs index 84189a1..d5fb406 100644 --- a/goas/cl/cl/src/nullifier.rs +++ b/goas/cl/cl/src/nullifier.rs @@ -65,6 +65,10 @@ impl NullifierCommitment { pub fn hex(&self) -> String { hex::encode(self.0) } + + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } } impl NullifierNonce { diff --git a/goas/cl/cl/src/partial_tx.rs b/goas/cl/cl/src/partial_tx.rs index afe9785..5610421 100644 --- a/goas/cl/cl/src/partial_tx.rs +++ b/goas/cl/cl/src/partial_tx.rs @@ -8,8 +8,8 @@ use crate::input::{Input, InputWitness}; use crate::merkle; use crate::output::{Output, OutputWitness}; -const MAX_INPUTS: usize = 8; -const MAX_OUTPUTS: usize = 8; +pub const MAX_INPUTS: usize = 8; +pub const MAX_OUTPUTS: usize = 8; /// The partial transaction commitment couples an input to a partial transaction. /// Prevents partial tx unbundling. diff --git a/goas/zone/Cargo.toml b/goas/zone/Cargo.toml index b8dd50d..7cc3b61 100644 --- a/goas/zone/Cargo.toml +++ b/goas/zone/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "common","host", "methods", "proof_statements", "risc0_proofs"] +members = [ "common", "executor", "proof_statements", "risc0_proofs"] # Always optimize; building and running the guest takes much longer without optimization. [profile.dev] diff --git a/goas/zone/common/Cargo.toml b/goas/zone/common/Cargo.toml index 921f2c8..3fdc733 100644 --- a/goas/zone/common/Cargo.toml +++ b/goas/zone/common/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } cl = { path = "../../cl/cl" } -proof_statements = { path = "../proof_statements", package = "goas_proof_statements" } \ No newline at end of file +goas_proof_statements = { path = "../proof_statements" } +proof_statements = { path = "../../cl/proof_statements" } +once_cell = "1" \ No newline at end of file diff --git a/goas/zone/common/src/lib.rs b/goas/zone/common/src/lib.rs index cca9cdc..88cc84b 100644 --- a/goas/zone/common/src/lib.rs +++ b/goas/zone/common/src/lib.rs @@ -1,4 +1,11 @@ -use cl::nullifier::{Nullifier, NullifierCommitment}; +use cl::{ + balance::Unit, + crypto, + input::InputWitness, + nullifier::{Nullifier, NullifierCommitment}, + output::OutputWitness, +}; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -11,6 +18,17 @@ pub const MAX_EVENTS: usize = 1 << 8; #[derive(Clone, Copy, Serialize, Deserialize)] pub struct StateCommitment([u8; 32]); +pub type AccountId = u32; + +// PLACEHOLDER: replace with the death constraint vk of the zone funds +pub const ZONE_FUNDS_VK: [u8; 32] = [0; 32]; +// PLACEHOLDER: this is probably going to be NMO? +pub static ZONE_CL_FUNDS_UNIT: Lazy = Lazy::new(|| crypto::hash_to_curve(b"NMO")); +// PLACEHOLDER +pub static ZONE_UNIT: Lazy = Lazy::new(|| crypto::hash_to_curve(b"ZONE_UNIT")); +// PLACEHOLDER +pub const ZONE_NF_PK: NullifierCommitment = NullifierCommitment::from_bytes([0; 32]); + #[derive(Clone, Serialize, Deserialize)] pub struct StateWitness { pub balances: BTreeMap, @@ -38,12 +56,8 @@ impl StateWitness { } fn included_txs_root(&self) -> [u8; 32] { - let tx_bytes = Vec::from_iter( - self.included_txs - .iter() - .map(Input::to_bytes) - .map(Vec::from_iter), - ); + // 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) } @@ -62,8 +76,8 @@ impl StateWitness { #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Withdraw { - pub from: u32, - pub amount: u32, + pub from: AccountId, + pub amount: AccountId, pub to: NullifierCommitment, pub nf: Nullifier, } @@ -79,22 +93,30 @@ impl Withdraw { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum Input { - Withdraw(Withdraw), +/// A deposit of funds into the zone +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Deposit { + /// The note that is used to deposit funds into the zone + pub deposit: InputWitness, + + // This zone state note + pub zone_note_in: InputWitness, + pub zone_note_out: OutputWitness, + + // The zone funds note + pub zone_funds_in: InputWitness, + pub zone_funds_out: OutputWitness, } -impl Input { - pub fn to_bytes(&self) -> Vec { - match self { - Input::Withdraw(withdraw) => withdraw.to_bytes().to_vec(), - } - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Input { + Withdraw(Withdraw), + Deposit(Deposit), } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Event { - Spend(proof_statements::zone_funds::Spend), + Spend(goas_proof_statements::zone_funds::Spend), } impl Event { diff --git a/goas/zone/host/Cargo.toml b/goas/zone/executor/Cargo.toml similarity index 84% rename from goas/zone/host/Cargo.toml rename to goas/zone/executor/Cargo.toml index 2864725..47ce798 100644 --- a/goas/zone/host/Cargo.toml +++ b/goas/zone/executor/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" default-run = "host" [dependencies] -methods = { path = "../methods" } +goas_risc0_proofs = { path = "../risc0_proofs", package = "goas_risc0_proofs" } risc0-zkvm = { version = "1.0", features = ["prove", "metal"] } risc0-groth16 = { version = "1.0" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/goas/zone/host/src/bin/groth16.rs b/goas/zone/executor/src/bin/groth16.rs similarity index 100% rename from goas/zone/host/src/bin/groth16.rs rename to goas/zone/executor/src/bin/groth16.rs diff --git a/goas/zone/host/src/main.rs b/goas/zone/executor/src/main.rs similarity index 93% rename from goas/zone/host/src/main.rs rename to goas/zone/executor/src/main.rs index 17cd386..90a68fe 100644 --- a/goas/zone/host/src/main.rs +++ b/goas/zone/executor/src/main.rs @@ -36,7 +36,7 @@ fn stf_prove_stark(state: StateWitness, inputs: Vec) { // This struct contains the receipt along with statistics about execution of the guest let opts = risc0_zkvm::ProverOpts::succinct(); let prove_info = prover - .prove_with_opts(env, methods::METHOD_ELF, &opts) + .prove_with_opts(env, goas_risc0_proofs::ZONE_STATE_ELF, &opts) .unwrap(); println!("STARK prover time: {:.2?}", start_t.elapsed()); @@ -48,7 +48,7 @@ fn stf_prove_stark(state: StateWitness, inputs: Vec) { std::fs::write("proof.stark", bincode::serialize(&receipt).unwrap()).unwrap(); // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. - receipt.verify(methods::METHOD_ID).unwrap(); + receipt.verify(goas_risc0_proofs::ZONE_STATE_ID).unwrap(); } fn main() { diff --git a/goas/zone/methods/Cargo.toml b/goas/zone/methods/Cargo.toml deleted file mode 100644 index 0a943cf..0000000 --- a/goas/zone/methods/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "methods" -version = "0.1.0" -edition = "2021" - -[build-dependencies] -risc0-build = { version = "1.0" } - -[package.metadata.risc0] -methods = ["guest"] diff --git a/goas/zone/methods/build.rs b/goas/zone/methods/build.rs deleted file mode 100644 index 08a8a4e..0000000 --- a/goas/zone/methods/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - risc0_build::embed_methods(); -} diff --git a/goas/zone/methods/guest/src/main.rs b/goas/zone/methods/guest/src/main.rs deleted file mode 100644 index ae9483f..0000000 --- a/goas/zone/methods/guest/src/main.rs +++ /dev/null @@ -1,40 +0,0 @@ -use common::*; -use proof_statements::zone_funds::Spend; -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 main() { - let inputs: Vec = env::read(); - let mut state: StateWitness = env::read(); - - for input in inputs { - match input { - Input::Withdraw(input) => { - state = withdraw(state, input); - } - } - } - - env::commit(&state.commit()); -} diff --git a/goas/zone/methods/src/lib.rs b/goas/zone/methods/src/lib.rs deleted file mode 100644 index 1bdb308..0000000 --- a/goas/zone/methods/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/goas/zone/proof_statements/src/zone_funds.rs b/goas/zone/proof_statements/src/zone_funds.rs index 5da4346..c44d8db 100644 --- a/goas/zone/proof_statements/src/zone_funds.rs +++ b/goas/zone/proof_statements/src/zone_funds.rs @@ -22,7 +22,6 @@ impl Spend { } } -/// There are two kind of paths #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SpendFundsPrivate { /// The note we're spending diff --git a/goas/zone/risc0_proofs/Cargo.toml b/goas/zone/risc0_proofs/Cargo.toml index e73bbbb..e7446b1 100644 --- a/goas/zone/risc0_proofs/Cargo.toml +++ b/goas/zone/risc0_proofs/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" risc0-build = { version = "1.0" } [package.metadata.risc0] -methods = ["spend_zone_funds"] +methods = ["spend_zone_funds", "zone_state"] diff --git a/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs b/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs index 7510e03..42f7f51 100644 --- a/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs +++ b/goas/zone/risc0_proofs/spend_zone_funds/src/main.rs @@ -60,12 +60,9 @@ fn main() { out_zone_funds.output.balance_blinding, in_zone_funds.input.balance_blinding ); - let mut evolved_nonce = [0; 16]; - evolved_nonce[..16] - .copy_from_slice(&Sha256::digest(&out_zone_funds.output.nonce.as_bytes())[..16]); assert_eq!( out_zone_funds.output.nonce, - NullifierNonce::from_bytes(evolved_nonce) + NullifierNonce::from_bytes(Sha256::digest(&out_zone_funds.output.nonce.as_bytes()).into()) ); assert_eq!(ptx_root, spent_note.ptx_root()); @@ -76,8 +73,5 @@ fn main() { // check the correct recipient is being paid assert_eq!(spent_note.output.nf_pk, spend_event.to); - env::commit(&DeathConstraintPublic { - ptx_root, - nf, - }); + env::commit(&DeathConstraintPublic { ptx_root, nf }); } diff --git a/goas/zone/methods/guest/Cargo.toml b/goas/zone/risc0_proofs/zone_state/Cargo.toml similarity index 85% rename from goas/zone/methods/guest/Cargo.toml rename to goas/zone/risc0_proofs/zone_state/Cargo.toml index 9c7aea4..ec3f512 100644 --- a/goas/zone/methods/guest/Cargo.toml +++ b/goas/zone/risc0_proofs/zone_state/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "method" +name = "zone_state" version = "0.1.0" edition = "2021" @@ -14,7 +14,9 @@ serde = { version = "1.0", features = ["derive"] } bincode = "1" common = { path = "../../common" } cl = { path = "../../../cl/cl" } -proof_statements = { path = "../../proof_statements", package = "goas_proof_statements" } +goas_proof_statements = { path = "../../proof_statements" } +proof_statements = { path = "../../../cl/proof_statements" } +sha2 = "0.10" [patch.crates-io] # Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint diff --git a/goas/zone/risc0_proofs/zone_state/src/main.rs b/goas/zone/risc0_proofs/zone_state/src/main.rs new file mode 100644 index 0000000..972196c --- /dev/null +++ b/goas/zone/risc0_proofs/zone_state/src/main.rs @@ -0,0 +1,140 @@ +use cl::{ + merkle, + nullifier::{Nullifier, NullifierNonce, NullifierSecret}, + partial_tx::{MAX_INPUTS, MAX_OUTPUTS}, + PtxRoot, +}; + +use common::*; +use goas_proof_statements::zone_funds::Spend; +use proof_statements::death_constraint::DeathConstraintPublic; +use risc0_zkvm::guest::env; +use sha2::{Digest, Sha256}; + +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, + pub_inputs: DeathConstraintPublic, +) -> StateWitness { + state.included_txs.push(Input::Deposit(deposit.clone())); + + let Deposit { + deposit, + zone_note_in, + zone_note_out, + zone_funds_in, + zone_funds_out, + } = deposit; + + // 1) Check there are no more input/output notes than expected + let inputs = [ + deposit.commit().to_bytes().to_vec(), + zone_note_in.commit().to_bytes().to_vec(), + zone_funds_in.commit().to_bytes().to_vec(), + ]; + + let inputs_root = merkle::root(merkle::padded_leaves::(&inputs)); + + let outputs = [ + zone_note_out.commit().to_bytes().to_vec(), + zone_funds_out.commit().to_bytes().to_vec(), + ]; + + let outputs_root = merkle::root(merkle::padded_leaves::(&outputs)); + + let ptx_root = PtxRoot(merkle::node(inputs_root, outputs_root)); + assert_eq!(ptx_root, pub_inputs.ptx_root); + + // 2) Check the deposit note is not already under control of the zone + assert_ne!(deposit.note.death_constraint, ZONE_FUNDS_VK); + + // 3) Check the ptx is balanced. This is not a requirement for standard ptxs, but we need it + // in deposits (at least in a first version) to ensure fund tracking + assert_eq!(deposit.note.unit, *ZONE_UNIT); + assert_eq!(zone_funds_in.note.unit, *ZONE_UNIT); + assert_eq!(zone_funds_out.note.unit, *ZONE_UNIT); + + let in_sum = deposit.note.value + zone_funds_in.note.value; + + let out_sum = zone_note_out.note.value; + + assert_eq!(out_sum, in_sum, "deposit ptx is unbalanced"); + + // 4) Check the zone fund notes are correctly created + assert_eq!(zone_funds_in.note.death_constraint, ZONE_FUNDS_VK); + assert_eq!(zone_funds_out.note.death_constraint, ZONE_FUNDS_VK); + assert_eq!(zone_funds_in.nf_sk, NullifierSecret::from_bytes([0; 16])); // there is no secret in the zone funds + assert_eq!(zone_funds_out.nf_pk, zone_funds_in.nf_sk.commit()); // the sk is the same + // nonce is correctly evolved + assert_eq!( + zone_funds_out.nonce, + NullifierNonce::from_bytes(Sha256::digest(&zone_funds_in.nonce.as_bytes()).into()) + ); + + // 5) Check zone state notes are correctly created + assert_eq!( + zone_note_in.note.death_constraint, + zone_note_out.note.death_constraint + ); + assert_eq!(zone_note_in.nf_sk, NullifierSecret::from_bytes([0; 16])); //// there is no secret in the zone state + assert_eq!(zone_note_out.nf_pk, zone_note_in.nf_sk.commit()); // the sk is the same + assert_eq!(zone_note_in.note.unit, zone_note_out.note.unit); + assert_eq!(zone_note_in.note.value, zone_note_out.note.value); + // nonce is correctly evolved + assert_eq!( + zone_note_out.nonce, + NullifierNonce::from_bytes(Sha256::digest(&zone_note_in.nonce.as_bytes()).into()) + ); + let nullifier = Nullifier::new(zone_note_in.nf_sk, zone_note_in.nonce); + assert_eq!(nullifier, pub_inputs.nf); + + // 6) We're now ready to do the deposit! + let amount = deposit.note.value as u32; + 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); + *to_balance = to_balance + .checked_add(amount) + .expect("overflow when depositing"); + + state +} + +fn main() { + let public_inputs: DeathConstraintPublic = env::read(); + let inputs: Vec = env::read(); + let mut state: StateWitness = env::read(); + + for input in inputs { + match input { + Input::Withdraw(input) => state = withdraw(state, input), + Input::Deposit(input) => state = deposit(state, input, public_inputs), + } + } + + env::commit(&state.commit()); +}