Skip to content

Commit

Permalink
test with dict
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeanmichel7 committed Sep 15, 2024
1 parent be2ea99 commit bfdf99b
Show file tree
Hide file tree
Showing 4 changed files with 467 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/consensus/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
247 changes: 247 additions & 0 deletions packages/consensus/src/types/test_utreexo.cairo
Original file line number Diff line number Diff line change
@@ -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<Nullable<OutPoint>>,
}

#[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<Nullable<felt252>>,
/// 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<OutPoint>, 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<TxOut> {} 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<OutPoint>, 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<felt252>,
}

/// 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<u64>,
/// List of sibling nodes required to calculate the root.
pub proof: Span<felt252>,
}

pub impl UtreexoStateDictDefaultDict of Default<UtreexoStateDict> {
fn default() -> UtreexoStateDict {
UtreexoStateDict { roots: Default::default(), num_leaves: 0, }
}
}

// impl UtreexoStateDictDisplayDict of Display<UtreexoStateDict> {
// 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<UtreexoProofDict> {
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<UtreexoBatchProofDict> {
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(())
}
}
89 changes: 87 additions & 2 deletions packages/consensus/src/types/utreexo.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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<Option<felt252>> = Default::default();
//TODO impl TxOutHash of Hash<TxOut> {} to remove cache
let mut n: felt252 = PoseidonTrait::new().update_with(outpoint.data).finalize();
let mut h = 0_usize;
let mut r: Option<felt252> = *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<OutPoint>, proof: @UtreexoBatchProof
) -> Result<(), UtreexoError> {
Result::Ok(())
}

fn delete_batch(ref self: UtreexoState, proof: @UtreexoBatchProof) {}
}

#[derive(Drop, Copy, PartialEq)]
pub enum UtreexoError {}

Expand All @@ -97,7 +182,7 @@ pub struct UtreexoBatchProof {

pub impl UtreexoStateDefault of Default<UtreexoState> {
fn default() -> UtreexoState {
UtreexoState { roots: array![].span(), num_leaves: 0, }
UtreexoState { roots: array![Option::None].span(), num_leaves: 0, }
}
}

Expand Down
Loading

0 comments on commit bfdf99b

Please sign in to comment.