goas: MMR for the zone transaction log

This commit is contained in:
David Rusu 2024-08-23 03:06:15 +04:00
parent 7a706583dc
commit 362749b1b6
8 changed files with 234 additions and 92 deletions

View File

@ -1,8 +1,11 @@
pub mod mmr;
use cl::{balance::Unit, merkle, NoteCommitment}; use cl::{balance::Unit, merkle, NoteCommitment};
use ed25519_dalek::{ use ed25519_dalek::{
ed25519::{signature::SignerMut, SignatureBytes}, ed25519::{signature::SignerMut, SignatureBytes},
Signature, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, Signature, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH,
}; };
use mmr::{MMRProof, MMR};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand_core::CryptoRngCore; use rand_core::CryptoRngCore;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -47,7 +50,7 @@ impl ZoneMetadata {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateWitness { pub struct StateWitness {
pub balances: BTreeMap<AccountId, u64>, pub balances: BTreeMap<AccountId, u64>,
pub included_txs: Vec<Tx>, pub included_txs: MMR,
pub zone_metadata: ZoneMetadata, pub zone_metadata: ZoneMetadata,
} }
@ -58,21 +61,25 @@ impl StateWitness {
pub fn state_roots(&self) -> StateRoots { pub fn state_roots(&self) -> StateRoots {
StateRoots { StateRoots {
tx_root: self.included_txs_root(), included_txs: self.included_txs.clone(),
zone_id: self.zone_metadata.id(), zone_id: self.zone_metadata.id(),
balance_root: self.balances_root(), balance_root: self.balances_root(),
} }
} }
pub fn apply(self, tx: Tx) -> Self { pub fn apply(self, tx: Tx) -> (Self, IncludedTxWitness) {
let mut state = match tx { let mut state = match tx {
Tx::Withdraw(w) => self.withdraw(w), Tx::Withdraw(w) => self.withdraw(w),
Tx::Deposit(d) => self.deposit(d), Tx::Deposit(d) => self.deposit(d),
}; };
state.included_txs.push(tx); let inclusion_proof = state.included_txs.push(&tx.to_bytes());
let tx_inclusion_proof = IncludedTxWitness {
tx,
proof: inclusion_proof,
};
state (state, tx_inclusion_proof)
} }
fn withdraw(mut self, w: Withdraw) -> Self { fn withdraw(mut self, w: Withdraw) -> Self {
@ -97,16 +104,6 @@ impl StateWitness {
self self
} }
pub fn included_txs_root(&self) -> [u8; 32] {
merkle::root::<MAX_TXS>(self.included_tx_merkle_leaves())
}
pub fn included_tx_witness(&self, idx: usize) -> IncludedTxWitness {
let tx = *self.included_txs.get(idx).unwrap();
let path = merkle::path(self.included_tx_merkle_leaves(), idx);
IncludedTxWitness { tx, path }
}
pub fn balances_root(&self) -> [u8; 32] { pub fn balances_root(&self) -> [u8; 32] {
let balance_bytes = Vec::from_iter(self.balances.iter().map(|(owner, balance)| { let balance_bytes = Vec::from_iter(self.balances.iter().map(|(owner, balance)| {
let mut bytes: Vec<u8> = vec![]; let mut bytes: Vec<u8> = vec![];
@ -121,15 +118,6 @@ impl StateWitness {
pub fn total_balance(&self) -> u64 { pub fn total_balance(&self) -> u64 {
self.balances.values().sum() self.balances.values().sum()
} }
fn included_tx_merkle_leaves(&self) -> [[u8; 32]; MAX_TXS] {
let tx_bytes = self
.included_txs
.iter()
.map(|t| t.to_bytes())
.collect::<Vec<_>>();
merkle::padded_leaves(&tx_bytes)
}
} }
impl From<StateCommitment> for [u8; 32] { impl From<StateCommitment> for [u8; 32] {
@ -237,31 +225,28 @@ impl Tx {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncludedTxWitness { pub struct IncludedTxWitness {
pub tx: Tx, pub tx: Tx,
pub path: Vec<merkle::PathNode>, pub proof: MMRProof,
}
impl IncludedTxWitness {
pub fn tx_root(&self) -> [u8; 32] {
let leaf = merkle::leaf(&self.tx.to_bytes());
merkle::path_root(leaf, &self.path)
}
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateRoots { pub struct StateRoots {
pub tx_root: [u8; 32], pub included_txs: MMR,
pub zone_id: [u8; 32], pub zone_id: [u8; 32],
pub balance_root: [u8; 32], pub balance_root: [u8; 32],
} }
impl StateRoots { impl StateRoots {
/// Merkle tree over: [txs, zoneid, balances] pub fn verify_tx_inclusion(&self, tx_inclusion: &IncludedTxWitness) -> bool {
self.included_txs
.verify_proof(&tx_inclusion.tx.to_bytes(), &tx_inclusion.proof)
}
/// Commitment to the state roots
pub fn commit(&self) -> StateCommitment { pub fn commit(&self) -> StateCommitment {
let leaves = cl::merkle::padded_leaves::<4>(&[ let mut hasher = Sha256::new();
self.tx_root.to_vec(), hasher.update(self.included_txs.commit());
self.zone_id.to_vec(), hasher.update(self.zone_id);
self.balance_root.to_vec(), hasher.update(self.balance_root);
]); StateCommitment(hasher.finalize().into())
StateCommitment(cl::merkle::root(leaves))
} }
} }

View File

@ -0,0 +1,162 @@
// use ckb_merkle_mountain_range::{util::MemStore, Merge, Result, MMR};
// use sha2::{Digest, Sha256};
// #[derive(Eq, PartialEq, Clone, Debug, Default)]
// struct NumberHash(pub [u8; 32]);
// impl From<u32> for NumberHash {
// fn from(num: u32) -> Self {
// let mut hasher = Sha256::new();
// hasher.update(num.to_le_bytes());
// NumberHash(hasher.finalize().into())
// }
// }
// struct MergeNumberHash;
// impl Merge for MergeNumberHash {
// type Item = NumberHash;
// fn merge(lhs: &Self::Item, rhs: &Self::Item) -> Result<Self::Item> {
// let mut hasher = Sha256::new();
// hasher.update(lhs.0);
// hasher.update(rhs.0);
// Ok(NumberHash(hasher.finalize().into()))
// }
// }
// fn prepare_mmr(count: u32) -> (u64, MemStore<NumberHash>, Vec<u64>) {
// let store = MemStore::default();
// let mut mmr = MMR::<_, MergeNumberHash, _>::new(0, &store);
// let positions: Vec<u64> = (0u32..count)
// .map(|i| mmr.push(NumberHash::from(i)).unwrap())
// .collect();
// let mmr_size = mmr.mmr_size();
// mmr.commit().expect("write to store");
// (mmr_size, store, positions)
// }
use cl::merkle;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MMR {
pub roots: Vec<Root>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Root {
pub root: [u8; 32],
pub height: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MMRProof {
pub path: Vec<merkle::PathNode>,
}
impl MMR {
pub fn new() -> Self {
Self { roots: vec![] }
}
pub fn push(&mut self, elem: &[u8]) -> MMRProof {
let new_root = Root {
root: merkle::leaf(elem),
height: 1,
};
self.roots.push(new_root);
let mut path = vec![];
for i in (1..self.roots.len()).rev() {
if self.roots[i].height == self.roots[i - 1].height {
path.push(merkle::PathNode::Left(self.roots[i - 1].root));
self.roots[i - 1] = Root {
root: merkle::node(self.roots[i - 1].root, self.roots[i].root),
height: self.roots[i - 1].height + 1,
};
self.roots.remove(i);
} else {
break;
}
}
MMRProof { path }
}
pub fn verify_proof(&self, elem: &[u8], proof: &MMRProof) -> bool {
let path_len = proof.path.len();
let leaf = merkle::leaf(elem);
let root = merkle::path_root(leaf, &proof.path);
for mmr_root in self.roots.iter() {
if mmr_root.height == (path_len + 1) as u64 {
return mmr_root.root == root;
}
}
false
}
pub fn commit(&self) -> [u8; 32] {
let mut hasher = Sha256::new();
for mrr_root in self.roots.iter() {
hasher.update(mrr_root.root);
hasher.update(mrr_root.height.to_le_bytes());
}
hasher.finalize().into()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_mrr_push() {
let mut mmr = MMR::new();
let proof = mmr.push(b"hello");
assert_eq!(mmr.roots.len(), 1);
assert_eq!(mmr.roots[0].height, 1);
assert_eq!(mmr.roots[0].root, merkle::leaf(b"hello"));
assert!(mmr.verify_proof(b"hello", &proof));
let proof = mmr.push(b"world");
assert_eq!(mmr.roots.len(), 1);
assert_eq!(mmr.roots[0].height, 2);
assert_eq!(
mmr.roots[0].root,
merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world"))
);
assert!(mmr.verify_proof(b"world", &proof));
let proof = mmr.push(b"!");
assert_eq!(mmr.roots.len(), 2);
assert_eq!(mmr.roots[0].height, 2);
assert_eq!(
mmr.roots[0].root,
merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world"))
);
assert_eq!(mmr.roots[1].height, 1);
assert_eq!(mmr.roots[1].root, merkle::leaf(b"!"));
assert!(mmr.verify_proof(b"!", &proof));
let proof = mmr.push(b"!");
assert_eq!(mmr.roots.len(), 1);
assert_eq!(mmr.roots[0].height, 3);
assert_eq!(
mmr.roots[0].root,
merkle::node(
merkle::node(merkle::leaf(b"hello"), merkle::leaf(b"world")),
merkle::node(merkle::leaf(b"!"), merkle::leaf(b"!"))
)
);
assert!(mmr.verify_proof(b"!", &proof));
}
}

View File

@ -1,6 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use common::{AccountId, SignedBoundTx, StateWitness, Tx, ZoneMetadata}; use common::{
mmr::MMR, AccountId, IncludedTxWitness, SignedBoundTx, StateWitness, Tx, ZoneMetadata,
};
use goas_proof_statements::{ use goas_proof_statements::{
user_note::UserAtomicTransfer, zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate, user_note::UserAtomicTransfer, zone_funds::SpendFundsPrivate, zone_state::ZoneStatePrivate,
}; };
@ -21,7 +23,7 @@ impl ZoneNotes {
) -> Self { ) -> Self {
let state = StateWitness { let state = StateWitness {
balances, balances,
included_txs: vec![], included_txs: MMR::new(),
zone_metadata: zone_metadata(zone_name), zone_metadata: zone_metadata(zone_name),
}; };
let state_note = zone_state_utxo(&state, &mut rng); let state_note = zone_state_utxo(&state, &mut rng);
@ -41,10 +43,9 @@ impl ZoneNotes {
cl::InputWitness::public(self.fund_note) cl::InputWitness::public(self.fund_note)
} }
pub fn run(mut self, txs: impl IntoIterator<Item = Tx>) -> Self { pub fn run(mut self, tx: Tx) -> (Self, IncludedTxWitness) {
for tx in txs { let (new_state, included_tx) = self.state.apply(tx);
self.state = self.state.apply(tx); self.state = new_state;
}
let state_in = self.state_input_witness(); let state_in = self.state_input_witness();
self.state_note = cl::OutputWitness::public( self.state_note = cl::OutputWitness::public(
@ -63,7 +64,8 @@ impl ZoneNotes {
}, },
state_in.evolved_nonce(b"FUND_NONCE"), state_in.evolved_nonce(b"FUND_NONCE"),
); );
self
(self, included_tx)
} }
} }
@ -231,26 +233,17 @@ mod tests {
pub fn test_prove_zone_stf() { pub fn test_prove_zone_stf() {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let zone_start = ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([]), &mut rng); let mut alice = common::new_account(&mut rng);
let alice_vk = alice.verifying_key().to_bytes();
let zone_start =
ZoneNotes::new_with_balances("ZONE", BTreeMap::from_iter([(alice_vk, 32)]), &mut rng);
let bind = OutputWitness::public( let bind = OutputWitness::public(
NoteWitness::basic(32, *common::ZONE_CL_FUNDS_UNIT), NoteWitness::basic(32, *common::ZONE_CL_FUNDS_UNIT),
cl::NullifierNonce::random(&mut rng), cl::NullifierNonce::random(&mut rng),
); );
let mut alice = common::new_account(&mut rng);
let alice_vk = alice.verifying_key().to_bytes();
let signed_deposit = SignedBoundTx::sign(
BoundTx {
tx: Tx::Deposit(Deposit {
to: alice_vk,
amount: 32,
}),
bind: bind.commit_note(),
},
&mut alice,
);
let signed_withdraw = SignedBoundTx::sign( let signed_withdraw = SignedBoundTx::sign(
BoundTx { BoundTx {
tx: Tx::Withdraw(Withdraw { tx: Tx::Withdraw(Withdraw {
@ -262,9 +255,7 @@ mod tests {
&mut alice, &mut alice,
); );
let zone_end = zone_start let zone_end = zone_start.clone().run(signed_withdraw.bound_tx.tx).0;
.clone()
.run([signed_deposit.bound_tx.tx, signed_withdraw.bound_tx.tx]);
let ptx = PartialTxWitness { let ptx = PartialTxWitness {
inputs: vec![ inputs: vec![
@ -276,10 +267,7 @@ mod tests {
balance_blinding: BalanceWitness::random_blinding(&mut rng), balance_blinding: BalanceWitness::random_blinding(&mut rng),
}; };
let txs = vec![ let txs = vec![(signed_withdraw, ptx.input_witness(0))];
(signed_deposit, ptx.input_witness(0)),
(signed_withdraw, ptx.input_witness(0)),
];
let proof = prove_zone_stf( let proof = prove_zone_stf(
zone_start.state.clone(), zone_start.state.clone(),
@ -322,9 +310,9 @@ mod tests {
let alice = common::new_account(&mut rng); let alice = common::new_account(&mut rng);
let alice_vk = alice.verifying_key().to_bytes(); let alice_vk = alice.verifying_key().to_bytes();
let mut zone_a = let zone_a =
ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice_vk, 40)]), &mut rng); ZoneNotes::new_with_balances("ZONE_A", BTreeMap::from_iter([(alice_vk, 40)]), &mut rng);
let mut zone_b = ZoneNotes::new_with_balances("ZONE_B", BTreeMap::new(), &mut rng); let zone_b = ZoneNotes::new_with_balances("ZONE_B", BTreeMap::new(), &mut rng);
let user_intent = UserIntent { let user_intent = UserIntent {
zone_a_meta: zone_a.state.zone_metadata, zone_a_meta: zone_a.state.zone_metadata,
@ -343,8 +331,8 @@ mod tests {
NullifierNonce::random(&mut rng), NullifierNonce::random(&mut rng),
)); ));
zone_a = zone_a.run([Tx::Withdraw(user_intent.withdraw)]); let (zone_a, withdraw_included_witnesss) = zone_a.run(Tx::Withdraw(user_intent.withdraw));
zone_b = zone_b.run([Tx::Deposit(user_intent.deposit)]); let (zone_b, deposit_included_witnesss) = zone_b.run(Tx::Deposit(user_intent.deposit));
let ptx = PartialTxWitness { let ptx = PartialTxWitness {
inputs: vec![user_note], inputs: vec![user_note],
@ -359,8 +347,8 @@ mod tests {
zone_b: ptx.output_witness(1), zone_b: ptx.output_witness(1),
zone_a_roots: zone_a.state.state_roots(), zone_a_roots: zone_a.state.state_roots(),
zone_b_roots: zone_b.state.state_roots(), zone_b_roots: zone_b.state.state_roots(),
withdraw_tx: zone_a.state.included_tx_witness(0), withdraw_tx: withdraw_included_witnesss,
deposit_tx: zone_b.state.included_tx_witness(0), deposit_tx: deposit_included_witnesss,
}; };
let proof = prove_user_atomic_transfer(user_atomic_transfer); let proof = prove_user_atomic_transfer(user_atomic_transfer);

View File

@ -46,13 +46,12 @@ fn test_atomic_transfer() {
balance_blinding: BalanceWitness::random_blinding(&mut rng), balance_blinding: BalanceWitness::random_blinding(&mut rng),
}; };
let zone_a_end = zone_a_start let (zone_a_end, withdraw_inclusion) = zone_a_start
.clone() .clone()
.run([Tx::Withdraw(alice_intent.withdraw)]); .run(Tx::Withdraw(alice_intent.withdraw));
let zone_b_end = zone_b_start let (zone_b_end, deposit_inclusion) =
.clone() zone_b_start.clone().run(Tx::Deposit(alice_intent.deposit));
.run([Tx::Deposit(alice_intent.deposit)]);
let alice_intent_in = cl::InputWitness::public(alice_intent_out); let alice_intent_in = cl::InputWitness::public(alice_intent_out);
let atomic_transfer_ptx = cl::PartialTxWitness { let atomic_transfer_ptx = cl::PartialTxWitness {
@ -97,8 +96,8 @@ fn test_atomic_transfer() {
zone_b: atomic_transfer_ptx.output_witness(2), zone_b: atomic_transfer_ptx.output_witness(2),
zone_a_roots: zone_a_end.state.state_roots(), zone_a_roots: zone_a_end.state.state_roots(),
zone_b_roots: zone_b_end.state.state_roots(), zone_b_roots: zone_b_end.state.state_roots(),
withdraw_tx: zone_a_end.state.included_tx_witness(0), withdraw_tx: withdraw_inclusion,
deposit_tx: zone_b_end.state.included_tx_witness(0), deposit_tx: deposit_inclusion,
}), }),
), ),
( (

View File

@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use cl::{BalanceWitness, NoteWitness, NullifierSecret}; use cl::{BalanceWitness, NoteWitness, NullifierSecret};
use common::{new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; use common::{mmr::MMR, new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT};
use executor::ZoneNotes; use executor::ZoneNotes;
use ledger::death_constraint::DeathProof; use ledger::death_constraint::DeathProof;
@ -20,7 +20,7 @@ fn test_deposit() {
amount: 78, amount: 78,
}; };
let zone_end = zone_start.clone().run([Tx::Deposit(deposit)]); let zone_end = zone_start.clone().run(Tx::Deposit(deposit)).0;
let alice_deposit = cl::InputWitness::from_output( let alice_deposit = cl::InputWitness::from_output(
cl::OutputWitness::random( cl::OutputWitness::random(
@ -82,7 +82,11 @@ fn test_deposit() {
zone_end.state_note.note.state, zone_end.state_note.note.state,
StateWitness { StateWitness {
balances: BTreeMap::from_iter([(alice_vk, 78)]), balances: BTreeMap::from_iter([(alice_vk, 78)]),
included_txs: vec![Tx::Deposit(deposit)], included_txs: {
let mut mmr = MMR::new();
mmr.push(&Tx::Deposit(deposit).to_bytes());
mmr
},
zone_metadata: zone_start.state.zone_metadata, zone_metadata: zone_start.state.zone_metadata,
} }
.commit() .commit()

View File

@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use cl::{BalanceWitness, NoteWitness, NullifierSecret}; use cl::{BalanceWitness, NoteWitness, NullifierSecret};
use common::{new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT}; use common::{mmr::MMR, new_account, BoundTx, SignedBoundTx, StateWitness, Tx, ZONE_CL_FUNDS_UNIT};
use executor::ZoneNotes; use executor::ZoneNotes;
use ledger::death_constraint::DeathProof; use ledger::death_constraint::DeathProof;
@ -30,7 +30,7 @@ fn test_withdrawal() {
amount: 78, amount: 78,
}; };
let zone_end = zone_start.clone().run([Tx::Withdraw(withdraw)]); let zone_end = zone_start.clone().run(Tx::Withdraw(withdraw)).0;
let alice_withdrawal = cl::OutputWitness::random( let alice_withdrawal = cl::OutputWitness::random(
NoteWitness::stateless( NoteWitness::stateless(
@ -102,7 +102,11 @@ fn test_withdrawal() {
zone_end.state_note.note.state, zone_end.state_note.note.state,
StateWitness { StateWitness {
balances: BTreeMap::from_iter([(alice_vk, 22)]), balances: BTreeMap::from_iter([(alice_vk, 22)]),
included_txs: vec![Tx::Withdraw(withdraw)], included_txs: {
let mut mmr = MMR::new();
mmr.push(&Tx::Withdraw(withdraw).to_bytes());
mmr
},
zone_metadata: zone_start.state.zone_metadata, zone_metadata: zone_start.state.zone_metadata,
} }
.commit() .commit()

View File

@ -7,9 +7,9 @@
/// ///
/// The Alice will create a partial tx that looks like this: /// The Alice will create a partial tx that looks like this:
/// ///
/// [fee note] -> [user note] /// [] -> [user note]
/// ///
/// The User Note will encode the logic that orchestrates the withdrawal from zone A /// Thep User Note will encode the logic that orchestrates the withdrawal from zone A
/// and deposit to zone B. /// and deposit to zone B.
/// ///
/// The User Notes death constraint requires the following statements to be satisfied /// The User Notes death constraint requires the following statements to be satisfied
@ -84,8 +84,8 @@ impl UserAtomicTransfer {
); );
// ensure txs were included in the respective zones // ensure txs were included in the respective zones
assert_eq!(self.withdraw_tx.tx_root(), self.zone_a_roots.tx_root); assert!(self.zone_a_roots.verify_tx_inclusion(&self.withdraw_tx));
assert_eq!(self.deposit_tx.tx_root(), self.zone_b_roots.tx_root); assert!(self.zone_b_roots.verify_tx_inclusion(&self.deposit_tx));
// ensure the txs are the same ones the user requested // ensure the txs are the same ones the user requested
assert_eq!( assert_eq!(

View File

@ -81,7 +81,7 @@ fn main() {
assert_eq!(ptx_input_witness.input_root(), input_root); assert_eq!(ptx_input_witness.input_root(), input_root);
// apply the ptx // apply the ptx
state = state.apply(bound_tx.tx) state = state.apply(bound_tx.tx).0
} }
validate_zone_transition(zone_in, zone_out, funds_out, in_state_cm, state); validate_zone_transition(zone_in, zone_out, funds_out, in_state_cm, state);