diff --git a/Scarb.lock b/Scarb.lock index 5f6475c2..a5d4d709 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -59,3 +59,6 @@ version = "0.1.0" [[package]] name = "utreexo" version = "0.1.0" +dependencies = [ + "utils", +] diff --git a/packages/consensus/src/validation/script.cairo b/packages/consensus/src/validation/script.cairo index cfcdf5d1..d0491ccb 100644 --- a/packages/consensus/src/validation/script.cairo +++ b/packages/consensus/src/validation/script.cairo @@ -1,4 +1,3 @@ -use shinigami_engine::errors::byte_array_err; use shinigami_engine::engine::EngineTrait; use shinigami_engine::engine::EngineImpl; use shinigami_engine::hash_cache::HashCacheImpl; diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo index bc37e86c..b3ee1c2a 100644 --- a/packages/utils/src/lib.cairo +++ b/packages/utils/src/lib.cairo @@ -4,6 +4,7 @@ pub mod double_sha256; pub mod hash; pub mod merkle_tree; pub mod numeric; +pub mod sort; // Let's use core non provable functions for now. Much faster. // pub mod sha256; diff --git a/packages/utils/src/numeric.cairo b/packages/utils/src/numeric.cairo index 1ad6c817..91f7d970 100644 --- a/packages/utils/src/numeric.cairo +++ b/packages/utils/src/numeric.cairo @@ -1,3 +1,5 @@ +use crate::bit_shifts::shr; + /// Reverses the byte order of a `u32`. /// /// This function takes a 32-bit unsigned integer and reverses the order of its bytes. @@ -10,9 +12,26 @@ pub fn u32_byte_reverse(word: u32) -> u32 { return byte0 + byte1 + byte2 + byte3; } +/// Computes the next power of two of a u64 variable. +pub fn u64_next_power_of_two(mut n: u64) -> u64 { + if n == 0 { + return 1; + } + + n -= 1; + n = n | shr(n, 1_u64); + n = n | shr(n, 2_u64); + n = n | shr(n, 4_u64); + n = n | shr(n, 8_u64); + n = n | shr(n, 16_u64); + n = n | shr(n, 32_u64); + + n + 1 +} + #[cfg(test)] mod tests { - use super::u32_byte_reverse; + use super::{u32_byte_reverse, u64_next_power_of_two}; #[test] fn test_u32_byte_reverse() { @@ -41,4 +60,67 @@ mod tests { let result = u32_byte_reverse(input); assert_eq!(result, expected_output); } + + #[test] + fn test_u64_next_power_of_two() { + let input: u64 = 3; + let expected_output: u64 = 4; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 5; + let expected_output: u64 = 8; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 11; + let expected_output: u64 = 16; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 20; + let expected_output: u64 = 32; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 61; + let expected_output: u64 = 64; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 100; + let expected_output: u64 = 128; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 189; + let expected_output: u64 = 256; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 480; + let expected_output: u64 = 512; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 777; + let expected_output: u64 = 1024; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 1025; + let expected_output: u64 = 2048; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 4095; + let expected_output: u64 = 4096; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + + let input: u64 = 1500000000000000000; + let expected_output: u64 = 2305843009213693952; + let result = u64_next_power_of_two(input); + assert_eq!(result, expected_output); + } } diff --git a/packages/utils/src/sort.cairo b/packages/utils/src/sort.cairo new file mode 100644 index 00000000..063956aa --- /dev/null +++ b/packages/utils/src/sort.cairo @@ -0,0 +1,39 @@ +/// Bubble sort from +/// https://github.com/keep-starknet-strange/alexandria/blob/main/packages/sorting/src/bubble_sort.cairo +pub fn bubble_sort, +Drop, +PartialOrd>(mut array: Span) -> Array { + if array.len() == 0 { + return array![]; + } + if array.len() == 1 { + return array![*array[0]]; + } + let mut idx1 = 0; + let mut idx2 = 1; + let mut sorted_iteration = true; + let mut sorted_array = array![]; + + loop { + if idx2 == array.len() { + sorted_array.append(*array[idx1]); + if sorted_iteration { + break; + } + array = sorted_array.span(); + sorted_array = array![]; + idx1 = 0; + idx2 = 1; + sorted_iteration = true; + } else { + if *array[idx1] <= *array[idx2] { + sorted_array.append(*array[idx1]); + idx1 = idx2; + idx2 += 1; + } else { + sorted_array.append(*array[idx2]); + idx2 += 1; + sorted_iteration = false; + } + }; + }; + sorted_array +} diff --git a/packages/utreexo/Scarb.toml b/packages/utreexo/Scarb.toml index 1bfcb576..e823b1f8 100644 --- a/packages/utreexo/Scarb.toml +++ b/packages/utreexo/Scarb.toml @@ -3,6 +3,9 @@ name = "utreexo" version = "0.1.0" edition = "2024_07" +[dependencies] +utils = { path = "../utils" } + [dev-dependencies] cairo_test.workspace = true diff --git a/packages/utreexo/src/stump/accumulator.cairo b/packages/utreexo/src/stump/accumulator.cairo index 5e991f62..0df03254 100644 --- a/packages/utreexo/src/stump/accumulator.cairo +++ b/packages/utreexo/src/stump/accumulator.cairo @@ -1,19 +1,44 @@ use crate::stump::state::UtreexoStumpState; -use crate::stump::proof::UtreexoBatchProof; +use crate::stump::proof::{UtreexoBatchProof, UtreexoBatchProofTrait}; #[generate_trait] pub impl StumpUtreexoAccumulatorImpl of StumpUtreexoAccumulator { - /// Adds multiple items to the accumulator + /// Adds multiple items to the accumulator. fn add(self: @UtreexoStumpState, hashes: Span) -> UtreexoStumpState { // TODO: check if vanilla implementation is compatible with rustreexo, add tests // https://github.com/mit-dci/rustreexo/blob/6a8fe53fa545df8f1a30733fa6ca9f7b0077d051/src/accumulator/stump.rs#L252 *self } + /// Verifies that one or multiple leaves hashes are part of the utreexo forest given a proof. fn verify( self: @UtreexoStumpState, proof: @UtreexoBatchProof, del_hashes: Span ) -> Result<(), ByteArray> { - // TODO + let computed_roots: Span = proof.compute_roots(del_hashes, *self.num_leaves)?; + let mut number_matched_roots: u32 = 0; + + for i in 0 + ..computed_roots + .len() { + for root in *self + .roots { + match root { + Option::Some(root) => { + if (computed_roots[i] == root) { + number_matched_roots += 1; + }; + }, + Option::None => {}, + }; + }; + }; + + let computed_roots_len = computed_roots.len(); + + if (computed_roots_len != number_matched_roots && computed_roots_len != 0) { + return Result::Err("Proof verification failed"); + } + Result::Ok(()) } @@ -22,3 +47,93 @@ pub impl StumpUtreexoAccumulatorImpl of StumpUtreexoAccumulator { *self } } + +#[cfg(test)] +mod tests { + use super::{UtreexoStumpState, StumpUtreexoAccumulator, UtreexoBatchProof}; + + #[test] + fn test_verification_1() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x371cb6995ea5e7effcd2e174de264b5b407027a75a231a70c2c8d196107f0e7) + ] + .span(), + num_leaves: 2 + }; + let batch_proof = UtreexoBatchProof { targets: array![0].span(), proof: array![2].span() }; + let del_hashes = array![1]; + + let result = state.verify(@batch_proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } + + #[test] + fn test_verification_2() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x1702d734e291ad551b886a70b96446b99e19e405511e71fb5edfc4d2d83ce92), + Option::Some(0x770ad1be69d195e821c8c35051b32492e71592e230b950a99ebf87e98967ca), + Option::Some(0x2392042cbfda7371c81c9d7b456563533c2d6998b9e690a0d97421e6ae51a98), + Option::Some(0xf), + ] + .span(), + num_leaves: 15 + }; + let batch_proof = UtreexoBatchProof { + targets: array![1, 3, 10, 13].span(), + proof: array![ + 0x1, + 0x3, + 0xC, + 0xD, + 0x436e91732c0a83fa238d71460463f4b1fe0dc0b1ebcbc10967a84cec9d13154, + 0xdc9cc50aff0bdadd82a05bbab54015a07fccf2a4e30fa528fdca5a35d5423f + ] + .span() + }; + let del_hashes = array![2, 4, 11, 14]; + + let result = state.verify(@batch_proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } + + #[test] + fn test_verification_3() { + let state = UtreexoStumpState { + roots: array![ + Option::Some(0x519631921e4905a63203f0cca7f6e6917082f30cef0930aa05bdc4323f6a398), + Option::Some(0x5198dcd61c969dfa8396dd27439ab776d120c2d67294fbcded0aa5f658f9150), + Option::Some(0x21d7ab8efac0146b5b47c8ad5431c3d14d9210319b0be7428fb2382ef115671), + Option::Some(0x74f794e653e00357d8a8ed45fcb74659841190c0821aa4e20bc4e30b2f3dd20), + ] + .span(), + num_leaves: 30 + }; + let batch_proof = UtreexoBatchProof { + targets: array![4, 8, 12, 16, 20, 24, 28].span(), + proof: array![ + 0x6, + 0xA, + 0xE, + 0x12, + 0x16, + 0x1A, + 0x1E, + 0x2797a40dbb8ea4b69a4e3bb4a9ccaa21a9585fcc71f3e5bb053ccae27910f90, + 0x7877cc14d4c8e76cc51aa4c49aa7aadaade0cf475ad63bb37c27c324e145393, + 0x556ea8bad1db13c6bdc3150a8289cd12044fb7e03cf201f35924a8afd4265a6, + 0x41a4ec75a27497daa51261588a60f0956d3fd61e521634bbf36bba6343c3a1b, + 0x3ba731d3734536d7cd5382cb4004ca4c24f1325b6fbeae27bcd6b4f9c0ed714, + 0x117ed04a65093683f13c16cf73d2855f1f099a96581d1dad74eaf34c9a343c8, + 0x79b32f615bbd57783700ae5f8e7b1ef79677c3545c4c69dc31b3aecce1d8fa6 + ] + .span() + }; + + let del_hashes = array![5, 9, 13, 17, 21, 25, 29]; + + let result = state.verify(@batch_proof, del_hashes.span(),); + assert_eq!(result, Result::Ok(())); + } +} diff --git a/packages/utreexo/src/stump/proof.cairo b/packages/utreexo/src/stump/proof.cairo index a74fea25..27cce7cf 100644 --- a/packages/utreexo/src/stump/proof.cairo +++ b/packages/utreexo/src/stump/proof.cairo @@ -1,10 +1,13 @@ use core::fmt::{Display, Formatter, Error}; +use core::num::traits::Bounded; +use crate::parent_hash; +use utils::{numeric::u64_next_power_of_two, sort::bubble_sort}; /// Utreexo inclusion proof for multiple outputs. /// Compatible with https://github.com/utreexo/utreexo #[derive(Drop, Copy)] pub struct UtreexoBatchProof { - /// List of sibling nodes required to calculate the root. + /// List of sibling nodes required to calculate the roots. pub proof: Span, /// Indices of leaves to be deleted (ordered starting from 0, left to right). pub targets: Span, @@ -27,3 +30,156 @@ impl UtreexoBatchProofDisplay of Display { Result::Ok(()) } } + +#[generate_trait] +pub impl UtreexoBatchProofImpl of UtreexoBatchProofTrait { + /// Computes a set of roots given a proof and leaves hashes. + fn compute_roots( + self: @UtreexoBatchProof, mut del_hashes: Span, num_leaves: u64, + ) -> Result, ByteArray> { + // Where all the parent hashes we've calculated in a given row will go to. + let mut calculated_root_hashes: Array = array![]; + // Target leaves + let mut leaf_nodes: Array<(u64, felt252)> = array![]; + + let mut inner_result = Result::Ok((array![].span())); + + // Append targets with their hashes. + let mut positions = *self.targets; + while let Option::Some(rhs) = del_hashes.pop_front() { + if let Option::Some(lhs) = positions.pop_front() { + leaf_nodes.append((*lhs, *rhs)); + } else { + inner_result = Result::Err("Not enough targets in the proof."); + } + }; + + let mut leaf_nodes: Array<(u64, felt252)> = bubble_sort(leaf_nodes.span()); + + // Proof nodes. + let mut sibling_nodes: Array = (*self.proof).into(); + // Queue of computed intermediate nodes. + let mut computed_nodes: Array<(u64, felt252)> = array![]; + // Actual length of the current row. + let mut actual_row_len: u64 = num_leaves; + // Length of the "padded" row which is always power of two. + let mut row_len: u64 = u64_next_power_of_two(num_leaves); + // Total padded length of processed rows (excluding the current one). + let mut row_len_acc: u64 = 0; + // Next position of the target leaf and the leaf itself. + let (mut next_leaf_pos, mut next_leaf) = leaf_nodes.pop_front().unwrap(); + // Next computed node. + let mut next_computed: felt252 = 0; + // Position of the next computed node. + let mut next_computed_pos: u64 = Bounded::::MAX; + + while row_len != 0 { + let (pos, node) = if next_leaf_pos < next_computed_pos { + let res = (next_leaf_pos, next_leaf); + let (a, b) = leaf_nodes.pop_front().unwrap_or((Bounded::::MAX, 0)); + next_leaf_pos = a; + next_leaf = b; + res + } else if next_computed_pos != Bounded::::MAX { + let res = (next_computed_pos, next_computed); + let (a, b) = computed_nodes.pop_front().unwrap_or((Bounded::::MAX, 0)); + next_computed_pos = a; + next_computed = b; + res + } else { + // Out of nodes, terminating here. + break; + }; + + // If we are beyond current row, level up. + while pos >= row_len_acc + row_len { + row_len_acc += row_len; + row_len /= 2; + actual_row_len /= 2; + + if row_len == 0 { + inner_result = + Result::Err( + format!("Position {pos} is out of the forest range {row_len_acc}.") + ); + break; + } + }; + + // If row length is odd and we are at the edge this is a root. + if pos == row_len_acc + actual_row_len - 1 && actual_row_len % 2 == 1 { + calculated_root_hashes.append(node); + row_len_acc += row_len; + row_len /= 2; + actual_row_len /= 2; + continue; + }; + + let parent_node = if (pos - row_len_acc) % 2 == 0 { + // Right sibling can be both leaf/computed or proof. + let right_sibling = if next_leaf_pos == pos + 1 { + let res = next_leaf; + let (a, b) = leaf_nodes.pop_front().unwrap_or((Bounded::::MAX, 0)); + next_leaf_pos = a; + next_leaf = b; + res + } else if next_computed_pos == pos + 1 { + let res = next_computed; + let (a, b) = computed_nodes.pop_front().unwrap_or((Bounded::::MAX, 0)); + next_computed_pos = a; + next_computed = b; + res + } else { + if sibling_nodes.is_empty() { + inner_result = Result::Err("Proof is empty."); + break; + }; + sibling_nodes.pop_front().unwrap() + }; + parent_hash(node, right_sibling) + } else { + // Left sibling always from proof. + if let Option::Some(left_sibling) = sibling_nodes.pop_front() { + parent_hash(left_sibling, node) + } else { + inner_result = Result::Err("Proof is empty."); + break; + } + }; + + let parent_pos = row_len_acc + row_len + (pos - row_len_acc) / 2; + + if next_computed_pos == Bounded::::MAX { + next_computed_pos = parent_pos; + next_computed = parent_node; + } else { + computed_nodes.append((parent_pos, parent_node)); + } + }; + + if !sibling_nodes.is_empty() { + return Result::Err("Proof should be empty"); + } + + if inner_result != Result::Ok((array![].span())) { + inner_result + } else { + Result::Ok((calculated_root_hashes.span())) + } + } +} + +/// PartialOrd implementation for tuple (u32, felt252). +impl PartialOrdTupleU64Felt252 of PartialOrd<(u64, felt252)> { + fn lt(lhs: (u64, felt252), rhs: (u64, felt252)) -> bool { + let (a, _) = lhs; + let (b, _) = rhs; + + if a < b { + true + } else { + false + } + } +} + diff --git a/packages/utreexo/src/vanilla/proof.cairo b/packages/utreexo/src/vanilla/proof.cairo index 3e9b8d9c..479925d1 100644 --- a/packages/utreexo/src/vanilla/proof.cairo +++ b/packages/utreexo/src/vanilla/proof.cairo @@ -27,7 +27,7 @@ impl UtreexoProofDisplay of Display { #[generate_trait] pub impl UtreexoProofImpl of UtreexoProofTrait { - /// Computes the root given a a proof and leaf hash. + /// Computes the root given a proof and leaf hash. /// /// Traverses the tree from leaf to root, hashing paired nodes. /// Proof order is bottom-up. Returns the computed root.