diff --git a/goas/atomic_asset_transfer/common/Cargo.toml b/goas/atomic_asset_transfer/common/Cargo.toml index 3fdc733..8c428bf 100644 --- a/goas/atomic_asset_transfer/common/Cargo.toml +++ b/goas/atomic_asset_transfer/common/Cargo.toml @@ -8,4 +8,5 @@ serde = { version = "1", features = ["derive"] } cl = { path = "../../cl/cl" } goas_proof_statements = { path = "../proof_statements" } proof_statements = { path = "../../cl/proof_statements" } -once_cell = "1" \ No newline at end of file +once_cell = "1" +sha2 = "0.10" \ No newline at end of file diff --git a/goas/atomic_asset_transfer/common/src/lib.rs b/goas/atomic_asset_transfer/common/src/lib.rs index a4bfbe8..dbc18c5 100644 --- a/goas/atomic_asset_transfer/common/src/lib.rs +++ b/goas/atomic_asset_transfer/common/src/lib.rs @@ -7,6 +7,7 @@ use cl::{ }; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::BTreeMap; // TODO: sparse merkle tree @@ -20,27 +21,49 @@ 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(Debug, Clone, Serialize, Deserialize)] +pub struct ZoneMetadata { + pub zone_vk: [u8; 32], + pub funds_vk: [u8; 32], + pub unit: Unit, +} + +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.unit.compress().as_bytes()); + hasher.finalize().into() + } +} #[derive(Clone, Serialize, Deserialize)] pub struct StateWitness { pub balances: BTreeMap, pub included_txs: Vec, pub output_events: Vec, + pub zone_metadata: ZoneMetadata, } impl StateWitness { + /// Merkle tree over: + /// root + /// / \ + /// io state + /// / \ / \ + /// events txs zoneid balances pub fn commit(&self) -> StateCommitment { - let root = self.balances_root(); - let root = cl::merkle::node(self.events_root(), root); - let root = cl::merkle::node(self.included_txs_root(), root); + let root = cl::merkle::root([ + self.events_root(), + self.included_txs_root(), + self.zone_metadata.id(), + self.balances_root(), + ]); + StateCommitment(root) } diff --git a/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs b/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs index c44d8db..0343902 100644 --- a/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs +++ b/goas/atomic_asset_transfer/proof_statements/src/zone_funds.rs @@ -34,6 +34,10 @@ pub struct SpendFundsPrivate { pub spent_note: PartialTxOutputPrivate, /// The event emitted by the zone that authorizes the spend pub spend_event: Spend, - /// Path to the zone output state + /// Path to the zone output events root pub spend_event_state_path: Vec, + /// Merkle root of txs included in the zone + pub txs_root: [u8; 32], + /// Merkle root of balances in the zone + pub balances_root: [u8; 32], } diff --git a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs index 42f7f51..d4b59de 100644 --- a/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs +++ b/goas/atomic_asset_transfer/risc0_proofs/spend_zone_funds/src/main.rs @@ -2,11 +2,10 @@ /// /// Our goal: prove the zone authorized spending of funds use cl::merkle; -use cl::nullifier::{Nullifier, NullifierNonce, NullifierSecret}; +use cl::nullifier::{Nullifier, NullifierSecret}; use goas_proof_statements::zone_funds::SpendFundsPrivate; use proof_statements::death_constraint::DeathConstraintPublic; use risc0_zkvm::guest::env; -use sha2::{Digest, Sha256}; fn main() { let SpendFundsPrivate { @@ -16,20 +15,34 @@ fn main() { spent_note, spend_event, spend_event_state_path, + txs_root, + balances_root, } = env::read(); let ptx_root = in_zone_funds.ptx_root(); let nf = Nullifier::new(in_zone_funds.input.nf_sk, in_zone_funds.input.nonce); // check the zone funds note is the one in the spend event assert_eq!(nf, spend_event.nf); - assert_eq!(ptx_root, zone_note.ptx_root()); - // assert the spent event was an output of the zone stf + + // ** Assert the spent event was an output of the correct zone stf ** + // The zone state field is a merkle tree over: + // root + // / \ + // io state + // / \ / \ + // events txs zoneid balances + // We need to check that: + // 1) There is a valid path from the spend event to the events root + // 2) The zone id matches the one in the current funds note state + // 3) The witnesses for spend path, txs and balances allow to calculate the correct root + let zone_id = in_zone_funds.input.note.state; // TODO: is there more state? let spend_event_leaf = merkle::leaf(&spend_event.to_bytes()); - // TODO: zones will have some more state + let event_root = merkle::path_root(spend_event_leaf, &spend_event_state_path); + assert_eq!( - zone_note.output.note.state, - merkle::path_root(spend_event_leaf, &spend_event_state_path) + merkle::root([event_root, txs_root, zone_id, balances_root]), + zone_note.output.note.state ); assert_eq!(ptx_root, out_zone_funds.ptx_root()); @@ -62,7 +75,12 @@ fn main() { ); assert_eq!( out_zone_funds.output.nonce, - NullifierNonce::from_bytes(Sha256::digest(&out_zone_funds.output.nonce.as_bytes()).into()) + in_zone_funds.input.evolved_nonce() + ); + // the state is propagated + assert_eq!( + out_zone_funds.output.note.state, + in_zone_funds.input.note.state ); assert_eq!(ptx_root, spent_note.ptx_root()); 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 b7d7dc7..50216c6 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,7 +1,7 @@ use cl::{ input::InputWitness, merkle, - nullifier::{Nullifier, NullifierNonce, NullifierSecret}, + nullifier::{Nullifier, NullifierSecret}, partial_tx::{MAX_INPUTS, MAX_OUTPUTS}, PtxRoot, }; @@ -13,7 +13,6 @@ use proof_statements::{ ptx::{PartialTxInputPrivate, PartialTxOutputPrivate}, }; use risc0_zkvm::guest::env; -use sha2::{Digest, Sha256}; fn withdraw(mut state: StateWitness, withdraw: Withdraw) -> StateWitness { state.included_txs.push(Input::Withdraw(withdraw)); @@ -54,6 +53,8 @@ fn deposit( zone_funds_out, } = deposit; + let funds_vk = state.zone_metadata.funds_vk; + // 1) Check there are no more input/output notes than expected let inputs = [ deposit.commit().to_bytes().to_vec(), @@ -74,13 +75,13 @@ fn deposit( 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); + assert_ne!(deposit.note.death_constraint, 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); + assert_eq!(deposit.note.unit, *ZONE_CL_FUNDS_UNIT); + assert_eq!(zone_funds_in.note.unit, *ZONE_CL_FUNDS_UNIT); + assert_eq!(zone_funds_out.note.unit, *ZONE_CL_FUNDS_UNIT); let in_sum = deposit.note.value + zone_funds_in.note.value; @@ -89,15 +90,14 @@ fn deposit( 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.note.death_constraint, funds_vk); + assert_eq!(zone_funds_out.note.death_constraint, funds_vk); + assert_eq!(zone_funds_in.note.state, state.zone_metadata.id()); + assert_eq!(zone_funds_out.note.state, state.zone_metadata.id()); 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()) - ); + assert_eq!(zone_funds_out.nonce, zone_funds_in.evolved_nonce()); // 5) Check zone state notes are correctly created assert_eq!( @@ -109,10 +109,7 @@ fn deposit( 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()) - ); + assert_eq!(zone_note_out.nonce, zone_note_in.evolved_nonce()); let nullifier = Nullifier::new(zone_note_in.nf_sk, zone_note_in.nonce); assert_eq!(nullifier, pub_inputs.nf); @@ -136,6 +133,11 @@ fn validate_zone_input( let nf = Nullifier::new(input.input.nf_sk, input.input.nonce); assert_eq!(input.input.note.state, <[u8; 32]>::from(state.commit())); + // should not be possible to create one but let's put this check here just in case + debug_assert_eq!( + input.input.note.death_constraint, + state.zone_metadata.zone_vk + ); (ptx_root, nf) } @@ -149,16 +151,13 @@ fn validate_zone_output( assert_eq!(ptx, output.ptx_root()); // the ptx root is the same as in the input let output = output.output; assert_eq!(output.note.state, <[u8; 32]>::from(state.commit())); // the state in the output is as calculated by this function - assert_eq!(output.note.death_constraint, input.note.death_constraint); // the death constraint is the same as the on in the input + assert_eq!(output.note.death_constraint, state.zone_metadata.zone_vk); // the death constraint is the correct one assert_eq!(output.nf_pk, NullifierSecret::from_bytes([0; 16]).commit()); // the nullifier secret is public assert_eq!(output.balance_blinding, input.balance_blinding); // the balance blinding is the same as in the input - assert_eq!(output.note.unit, input.note.unit); // the balance unit is the same as in the input + assert_eq!(output.note.unit, state.zone_metadata.unit); // the balance unit is the same as in the input // the nonce is correctly evolved - assert_eq!( - output.nonce, - NullifierNonce::from_bytes(Sha256::digest(&input.nonce.as_bytes()).into()) - ); + assert_eq!(output.nonce, input.evolved_nonce()); } fn main() { diff --git a/goas/cl/cl/src/input.rs b/goas/cl/cl/src/input.rs index 17b495c..af056a2 100644 --- a/goas/cl/cl/src/input.rs +++ b/goas/cl/cl/src/input.rs @@ -41,13 +41,17 @@ impl InputWitness { } } + pub fn evolved_nonce(&self) -> NullifierNonce { + self.nonce.evolve(&self.nf_sk) + } + pub fn evolve_output(&self, balance_blinding: BalanceWitness) -> crate::OutputWitness { - crate::OutputWitness { - note: self.note, - balance_blinding, - nf_pk: self.nf_sk.commit(), - nonce: self.nonce.evolve(&self.nf_sk), - } + crate::OutputWitness { + note: self.note, + balance_blinding, + nf_pk: self.nf_sk.commit(), + nonce: self.evolved_nonce(), + } } pub fn nullifier(&self) -> Nullifier { @@ -63,7 +67,7 @@ impl InputWitness { } pub fn note_commitment(&self) -> crate::NoteCommitment { - self.note.commit(self.nf_sk.commit(), self.nonce) + self.note.commit(self.nf_sk.commit(), self.nonce) } }