diff --git a/packages/consensus/src/lib.cairo b/packages/consensus/src/lib.cairo index 238e5741..780e37d4 100644 --- a/packages/consensus/src/lib.cairo +++ b/packages/consensus/src/lib.cairo @@ -10,6 +10,7 @@ pub mod validation { pub mod codec; pub mod types { pub mod utreexo; + pub mod test_utreexo; // to remove pub mod chain_state; pub mod block; pub mod transaction; diff --git a/packages/consensus/src/types/test_utreexo.cairo b/packages/consensus/src/types/test_utreexo.cairo new file mode 100644 index 00000000..d0b8cb0c --- /dev/null +++ b/packages/consensus/src/types/test_utreexo.cairo @@ -0,0 +1,247 @@ +//! Utreexo is an accumulator for the Bitcoin unspent transaction set. +//! +//! It allows to verify that a certain transaction output exists +//! and still unspent at a particular block while maintaining only +//! a very compact state. +//! +//! It is also useful for transaction validation (our case) since it +//! allows to "fetch" the output spent by a particular input in the +//! validated transaction. This is typically required to calculate +//! transaction fee and also to check that script execution succeeds. +//! +//! The expected workflow is the following: +//! - For coinbase and inputs spending TXOs created in the same block +//! utreexo accumulator is not updated (local cache is used instead); +//! - For all other inputs we provide respective TXOs (extended) plus +//! plus inclusion proof (can be individual, batched, or hybrid); +//! - The client has to verify the inclusion proof and then remove all +//! the TXOs from the utreexo set, that way updating the state; +//! - For every output that is not spent in the same block we add the +//! extended (additionally contains txid and output index aka vout) output +//! to the accumulator (i.e. update the utreexo state). +//! +//! Note that utreexo data and proofs are provided via program input so +//! it is not part of prover/verifier arguments. Utreexo state (which +//! is part of the chain state) is what allows us to constrain +//! these inputs and ensure integrity. +//! +//! Read more about utreexo: https://eprint.iacr.org/2019/611.pdf + +use super::transaction::OutPoint; +use core::poseidon::PoseidonTrait; +use core::hash::{HashStateTrait, HashStateExTrait}; +use core::fmt::{Display, Formatter, Error}; +use utils::hash::Digest; +use core::nullable::{NullableTrait}; +use core::dict::Felt252Dict; + + +#[derive(Destruct, Default)] +pub struct UtxoSetDictTest { + /// Utreexo state. + pub utreexo_state: UtreexoStateDict, + /// Hashes of UTXOs created within the current block(s). + /// Note that to preserve the ordering, cache has to be updated right after a + /// particular output is created or spent. + cache: Felt252Dict>, +} + +#[generate_trait] +pub impl UtxoSetDictTestImpl of UtxoSetDictTestTrait { + fn new(utreexo_state: UtreexoStateDict) -> UtxoSetDictTest { + UtxoSetDictTest { utreexo_state, ..Default::default() } + } + + fn add(ref self: UtxoSetDictTest, outpoint: OutPoint) { + if outpoint.data.cached { + self.cache.insert(outpoint.block_height.into(), NullableTrait::new(outpoint)); + } else { + self.utreexo_state.add(outpoint); + } + } + + fn delete(ref self: UtxoSetDictTest, outpoint: @OutPoint) { + if *outpoint.data.cached { // TODO: remove from cache (+ verify inclusion) + } else { // TODO: update utreexo roots (+ verify inclusion) + // If batched proofs are used then do nothing + } + } +} + + +/// Accumulator representation of the state aka "Compact State Node". +/// Part of the chain state. +#[derive(Destruct)] +pub struct UtreexoStateDict { + /// Roots of the Merkle tree forest. + /// Index is the root height, None means a gap. + pub roots: Felt252Dict>, + /// Total number of leaves (in the bottom-most row). + /// Required to calculate the number of nodes in a particular row. + /// Can be reconstructed from the roots, but cached for convenience. + pub num_leaves: u64, +} + +/// Accumulator interface +pub trait UtreexoAccumulatorDict { + /// Adds single output to the accumulator. + /// The order *is important*: adding A,B and B,A would result in different states. + /// + /// Note that this call also pushes old UTXOs "to the left", to a larger subtree. + /// This mechanism ensures that short-lived outputs have small inclusion proofs. + fn add(ref self: UtreexoStateDict, outpoint: OutPoint); + + /// Verifies inclusion proof for a single output. + fn verify( + self: @UtreexoStateDict, output: @OutPoint, proof: @UtreexoProofDict + ) -> Result<(), UtreexoError>; + + /// Removes single output from the accumlator (order is important). + /// + /// Note that once verified, the output itself is not required for deletion, + /// the leaf index plus inclusion proof is enough. + fn delete(ref self: UtreexoStateDict, proof: @UtreexoProofDict); + + /// Verifies batch proof for multiple outputs (e.g. all outputs in a block). + fn verify_batch( + self: @UtreexoStateDict, outputs: Span, proof: @UtreexoBatchProofDict + ) -> Result<(), UtreexoError>; + + /// Removes multiple outputs from the accumulator. + fn delete_batch(ref self: UtreexoStateDict, proof: @UtreexoBatchProofDict); +} + +pub fn parent_hash(left: felt252, right: felt252, _block_hash: Digest) -> felt252 { + let parent_data = (left, right); // gas 1316516 + + // PoseidonTrait::new().update(left).update(right).update(block_hash).finalize() + PoseidonTrait::new().update_with(parent_data).finalize() +} + +struct LeafHash { + value: u64, + pk_script: @ByteArray +} + +pub impl UtreexoAccumulatorDictImpl of UtreexoAccumulatorDict { + // n ← leaf . n is initially the leaf to add + // h ← 0 . height is initially 0 + // r ← acc[h] . r is the lowest root + // while r != ∅ do . loop until an empty root + // n ← Parent(r, n) + // acc[h] ← ∅ + // h ← h + 1 + // r ← acc[h] + // acc[h] ← n + + fn add(ref self: UtreexoStateDict, outpoint: OutPoint) { + //TODO impl TxOutHash of Hash {} to remove cache + let mut n: felt252 = PoseidonTrait::new().update_with(outpoint.data).finalize(); + + let mut h = 0; + let mut r = self.roots.get(h); + + loop { + if (r.is_null()) { + break; + } + n = parent_hash(r.deref(), n, 0x01_u256.into()); + self.roots.insert(h, Default::default()); + h += 1; + r = self.roots.get(h); + }; + + self.roots.insert(h, NullableTrait::new(n)); + // println!("add leaf to dict: {h}: {n}"); + self.num_leaves += 1_u64; + } + + fn verify( + self: @UtreexoStateDict, output: @OutPoint, proof: @UtreexoProofDict + ) -> Result<(), UtreexoError> { + Result::Ok(()) + } + + fn delete(ref self: UtreexoStateDict, proof: @UtreexoProofDict) {} + + fn verify_batch( + self: @UtreexoStateDict, outputs: Span, proof: @UtreexoBatchProofDict + ) -> Result<(), UtreexoError> { + Result::Ok(()) + } + + fn delete_batch(ref self: UtreexoStateDict, proof: @UtreexoBatchProofDict) {} +} + +// #[derive(Drop, Copy, PartialEq)] +pub enum UtreexoError {} + +/// Utreexo inclusion proof for a single transaction output. +#[derive(Drop, Copy)] +pub struct UtreexoProofDict { + /// Index of the leaf in the forest, but also an encoded binary path, + /// specifying which sibling node is left and which is right. + pub leaf_index: u64, + /// List of sibling nodes required to calculate the root. + pub proof: Span, +} + +/// Utreexo inclusion proof for multiple outputs. +/// Compatible with https://github.com/utreexo/utreexo +#[derive(Drop, Copy)] +pub struct UtreexoBatchProofDict { + /// Indices of leaves to be deleted (ordered starting from 0, left to right). + pub targets: Span, + /// List of sibling nodes required to calculate the root. + pub proof: Span, +} + +pub impl UtreexoStateDictDefaultDict of Default { + fn default() -> UtreexoStateDict { + UtreexoStateDict { roots: Default::default(), num_leaves: 0, } + } +} + +// impl UtreexoStateDictDisplayDict of Display { +// fn fmt(self: @UtreexoStateDict, ref f: Formatter) -> Result<(), Error> { +// let str: ByteArray = format!( +// "UtreexoStateDict {{ roots: {}, num_leaves: {}, }}", +// (*self.roots).len(), +// *self.num_leaves +// ); +// f.buffer.append(@str); +// Result::Ok(()) +// } +// } + +impl UtreexoProofDictDisplayDict of Display { + fn fmt(self: @UtreexoProofDict, ref f: Formatter) -> Result<(), Error> { + let mut proofs: ByteArray = Default::default(); + for proof in *self.proof { + proofs.append(@format!("{},", proof)); + }; + let str: ByteArray = format!( + "UtreexoProofDict {{ leaf_index: {}, proof: {}, }}", *self.leaf_index, @proofs + ); + f.buffer.append(@str); + Result::Ok(()) + } +} + +impl UtreexoBatchProofDictDisplayDict of Display { + fn fmt(self: @UtreexoBatchProofDict, ref f: Formatter) -> Result<(), Error> { + let mut targets: ByteArray = Default::default(); + let mut proofs: ByteArray = Default::default(); + for target in *self.targets { + targets.append(@format!("{},", target)); + }; + for proof in *self.proof { + proofs.append(@format!("{},", proof)); + }; + let str: ByteArray = format!( + "UtreexoBatchProofDict {{ leaf_index: [{}], proof: [{}] }}", @targets, @proofs + ); + f.buffer.append(@str); + Result::Ok(()) + } +} diff --git a/packages/consensus/src/types/utreexo.cairo b/packages/consensus/src/types/utreexo.cairo index 5ae1468e..d5bba912 100644 --- a/packages/consensus/src/types/utreexo.cairo +++ b/packages/consensus/src/types/utreexo.cairo @@ -28,7 +28,10 @@ //! Read more about utreexo: https://eprint.iacr.org/2019/611.pdf use super::transaction::OutPoint; +use core::poseidon::PoseidonTrait; +use core::hash::{HashStateTrait, HashStateExTrait}; use core::fmt::{Display, Formatter, Error}; +use utils::hash::Digest; /// Accumulator representation of the state aka "Compact State Node". /// Part of the chain state. @@ -50,7 +53,7 @@ pub trait UtreexoAccumulator { /// /// Note that this call also pushes old UTXOs "to the left", to a larger subtree. /// This mechanism ensures that short-lived outputs have small inclusion proofs. - fn add(ref self: UtreexoState, output: OutPoint); + fn add(ref self: UtreexoState, outpoint: OutPoint); /// Verifies inclusion proof for a single output. fn verify( @@ -72,6 +75,88 @@ pub trait UtreexoAccumulator { fn delete_batch(ref self: UtreexoState, proof: @UtreexoBatchProof); } +// https://eprint.iacr.org/2019/611.pdf +// p18 +// To prevent such an attack, we require that the data inserted into the +// accumulator be not just the hash of a TXO, which is controllable by the +// attacker, but instead the concatenation of the TXO data with the block +// hash in which the TXO is confirmed. The attacker does not know the block +// hash before the TXO is confirmed, and it is not alterable by the attacker +// after confirmation (without significant cost). Verifiers, when inserting into +// the accumulator, perform this concatenation themselves after checking the +// proof of work of the block. Inclusion proofs contain this block hash data so +// that the leaf hash value can be correctly computed. +fn parent_hash(left: felt252, right: felt252, _block_hash: Digest) -> felt252 { + let parent_data = (left, right); + + // PoseidonTrait::new().update(left).update(right).update(block_hash).finalize() + PoseidonTrait::new().update_with(parent_data).finalize() +} + +pub impl UtreexoAccumulatorImpl of UtreexoAccumulator { + // n ← leaf . n is initially the leaf to add + // h ← 0 . height is initially 0 + // r ← acc[h] . r is the lowest root + // while r != ∅ do . loop until an empty root + // n ← Parent(r, n) + // acc[h] ← ∅ + // h ← h + 1 + // r ← acc[h] + // acc[h] ← n + + fn add(ref self: UtreexoState, outpoint: OutPoint) { + let mut new_roots: Array> = Default::default(); + //TODO impl TxOutHash of Hash {} to remove cache + let mut n: felt252 = PoseidonTrait::new().update_with(outpoint.data).finalize(); + let mut h = 0_usize; + let mut r: Option = *self.roots[h]; + let len = self.roots.len(); + + // loop until an empty root and compute new root + while r.is_some() { + n = parent_hash(r.unwrap(), n, 0x1_u256.into()); + new_roots.append(Option::None); + h += 1; + r = *self.roots[h]; + }; + + // add new root to height h + // println!("add leaf to array: {h}: {n}"); + new_roots.append(Option::Some(n)); + + // add None, which represents the end of the array if we add leaf to the highest one + if (h == len - 1) { + new_roots.append(Option::None); + } + + // copy the rest of the table + h += 1; + while h < len { + new_roots.append(*self.roots[h]); + h += 1; + }; + + self.roots = new_roots.span(); + self.num_leaves += 1_u64; + } + + fn verify( + self: @UtreexoState, output: @OutPoint, proof: @UtreexoProof + ) -> Result<(), UtreexoError> { + Result::Ok(()) + } + + fn delete(ref self: UtreexoState, proof: @UtreexoProof) {} + + fn verify_batch( + self: @UtreexoState, outputs: Span, proof: @UtreexoBatchProof + ) -> Result<(), UtreexoError> { + Result::Ok(()) + } + + fn delete_batch(ref self: UtreexoState, proof: @UtreexoBatchProof) {} +} + #[derive(Drop, Copy, PartialEq)] pub enum UtreexoError {} @@ -97,7 +182,7 @@ pub struct UtreexoBatchProof { pub impl UtreexoStateDefault of Default { fn default() -> UtreexoState { - UtreexoState { roots: array![].span(), num_leaves: 0, } + UtreexoState { roots: array![Option::None].span(), num_leaves: 0, } } } diff --git a/packages/consensus/src/types/utxo_set.cairo b/packages/consensus/src/types/utxo_set.cairo index e91f7480..4f9fba4b 100644 --- a/packages/consensus/src/types/utxo_set.cairo +++ b/packages/consensus/src/types/utxo_set.cairo @@ -9,18 +9,19 @@ //! In order to prove that the UTXOs provided actually belong to the set we use either //! Utreexo accumulator or local cache. +//input contain outpoint contain output use core::dict::Felt252Dict; -use super::utreexo::UtreexoState; use super::transaction::OutPoint; +use super::utreexo::{UtreexoState, UtreexoAccumulator}; -#[derive(Default)] +#[derive(Destruct, Default)] pub struct UtxoSet { /// Utreexo state. utreexo_state: UtreexoState, /// Hashes of UTXOs created within the current block(s). /// Note that to preserve the ordering, cache has to be updated right after a /// particular output is created or spent. - cache: Felt252Dict<()>, + cache: Felt252Dict>, } #[generate_trait] @@ -29,16 +30,139 @@ pub impl UtxoSetImpl of UtxoSetTrait { UtxoSet { utreexo_state, ..Default::default() } } - fn add(ref self: UtxoSet, output: OutPoint) { - if output.data.cached { // TODO: add to the local block cache - } else { // TODO: update utreexo roots + fn add(ref self: UtxoSet, outpoint: OutPoint) { + if outpoint.data.cached { + self.cache.insert(outpoint.block_height.into(), NullableTrait::new(outpoint)); + } else { + self.utreexo_state.add(outpoint); } } - fn delete(ref self: UtxoSet, output: @OutPoint) { - if *output.data.cached { // TODO: remove from cache (+ verify inclusion) + fn delete(ref self: UtxoSet, outpoint: @OutPoint) { + if *outpoint.data.cached { // TODO: remove from cache (+ verify inclusion) } else { // TODO: update utreexo roots (+ verify inclusion) // If batched proofs are used then do nothing } } } + +#[cfg(test)] +mod tests { + use super::{UtxoSet, UtxoSetTrait, OutPoint}; + use consensus::types::transaction::TxOut; + + // for dict test + use consensus::types::test_utreexo::{UtxoSetDictTest, UtxoSetDictTestTrait}; + + #[test] + fn test_utreexo_add1() { + let mut utxo_set: UtxoSet = UtxoSetTrait::new(Default::default()); + + // https://learnmeabitcoin.com/explorer/tx/b1fea52486ce0c62bb442b530a3f0132b826c74e473d1f2c220bfa78111c5082#input-0 + // coinbase outpoint + let outpoint1 = OutPoint { + txid: 0x0000000000000000000000000000000000000000000000000000000000000000_u256.into(), + vout: 4294967295, + data: TxOut { value: 0, pk_script: @"0x", cached: false }, + block_height: 0, + block_time: 0, + is_coinbase: false + }; + + // https://learnmeabitcoin.com/explorer/tx/f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16#input-0 + let outpoint2 = OutPoint { + txid: 0x0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9_u256.into(), + vout: 0, + data: TxOut { + // https://learnmeabitcoin.com/explorer/tx/0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9#output-0 + value: 5000000000, + pk_script: @"0x410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac", + cached: false + }, + block_height: 9, + block_time: 1231473279, + is_coinbase: true + }; + + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint2); + utxo_set.add(outpoint2); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + utxo_set.add(outpoint1); + + assert_eq!(utxo_set.utreexo_state.num_leaves, 30); + + for i in 0 + ..utxo_set + .utreexo_state + .roots + .len() { + println!("roots[{i}]: {:?}", utxo_set.utreexo_state.roots[i]); + }; + + let mut utxo_set_dict_test: UtxoSetDictTest = UtxoSetDictTestTrait::new(Default::default()); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint2); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + utxo_set_dict_test.add(outpoint1); + + assert_eq!(utxo_set_dict_test.utreexo_state.num_leaves, 30); + + for i in 0 + ..10_usize { + println!("roots[{i}]: {:?}", utxo_set_dict_test.utreexo_state.roots.get(i.into())); + } + } +} +