Skip to content

Commit

Permalink
feat(550): Implement block hashing (#589)
Browse files Browse the repository at this point in the history
  • Loading branch information
pierre-l authored Feb 9, 2024
1 parent c1540a4 commit 2011845
Show file tree
Hide file tree
Showing 5 changed files with 388 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ethabi = "18.0.0"
ethers = "2.0.11"
eyre.workspace = true
helios = { git = "https://github.com/a16z/helios", rev = "4ca6146eff964326240f2ef77215413b490d8301" }
lazy_static = "1.4.0"
reqwest = "0.11.13"
serde = { workspace = true, features = ["derive"] }
serde_with.workspace = true
Expand Down
183 changes: 183 additions & 0 deletions crates/core/src/block_hash/merkle_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//! Ported from [Papyrus](https://github.com/starkware-libs/papyrus/blob/v0.2.0/crates/papyrus_common/src/patricia_hash_tree.rs)
//! Patricia hash tree implementation.
//!
//! Supports root hash calculation for Stark felt values, keyed by consecutive 64 bits numbers,
//! starting from 0.
//!
//! Each edge is marked with one or more bits.
//! The key of a node is the concatenation of the edges' marks in the path from the root to this
//! node.
//! The input keys are in the leaves, and each leaf is an input key.
//!
//! The edges coming out of an internal node with a key `K` are:
//! - If there are input keys that start with 'K0...' and 'K1...', then two edges come out, marked
//! with '0' and '1' bits.
//! - Otherwise, a single edge mark with 'Z' is coming out. 'Z' is the longest string, such that all
//! the input keys that start with 'K...' start with 'KZ...' as well. Note, the order of the input
//! keys in this implementation forces 'Z' to be a zeros string.
//!
//! Hash of a node depends on the number of edges coming out of it:
//! - A leaf: The hash is the input value of its key.
//! - A single edge: pedersen_hash(child_hash, edge_mark) + edge_length.
//! - '0' and '1' edges: pedersen_hash(zero_child_hash, one_child_hash).
use bitvec::prelude::{BitArray, Msb0};
use starknet_crypto::{pedersen_hash, FieldElement};

const TREE_HEIGHT: u8 = 64;
type BitPath = BitArray<[u8; 8], Msb0>;

// An entry in a Patricia tree.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Entry {
key: BitPath,
value: FieldElement,
}

// A sub-tree is defined by a sub-sequence of leaves with a common ancestor at the specified height,
// with no other leaves under it besides these.
#[derive(Debug)]
struct SubTree<'a> {
leaves: &'a [Entry],
// Levels from the root.
height: u8,
}

enum SubTreeSplitting {
// Number of '0' bits that all the keys start with.
CommonZerosPrefix(u8),
// The index of the first key that starts with a '1' bit.
PartitionPoint(usize),
}

/// Calculates Patricia hash root on the given values.
/// The values are keyed by consecutive numbers, starting from 0.
pub fn calculate_root(values: Vec<FieldElement>) -> FieldElement {
if values.is_empty() {
return FieldElement::ZERO;
}
let leaves: Vec<Entry> = values
.into_iter()
.zip(0u64..)
.map(|(felt, idx)| Entry { key: idx.to_be_bytes().into(), value: felt })
.collect();
get_hash(SubTree { leaves: &leaves[..], height: 0_u8 })
}

// Recursive hash calculation. There are 3 cases:
// - Leaf: The sub tree height is maximal. It should contain exactly one entry.
// - Edge: All the keys start with a longest common ('0's) prefix. NOTE: We assume that the keys are
// a continuous range, and hence the case of '1's in the longest common prefix is impossible.
// - Binary: Some keys start with '0' bit and some start with '1' bit.
fn get_hash(sub_tree: SubTree<'_>) -> FieldElement {
if sub_tree.height == TREE_HEIGHT {
return sub_tree
.leaves
.first()
.expect("a leaf should not be empty")
.value;
}
match get_splitting(&sub_tree) {
SubTreeSplitting::CommonZerosPrefix(n_zeros) => {
get_edge_hash(sub_tree, n_zeros)
}
SubTreeSplitting::PartitionPoint(partition_point) => {
get_binary_hash(sub_tree, partition_point)
}
}
}

// Hash on a '0's sequence with the bottom sub tree.
fn get_edge_hash(sub_tree: SubTree<'_>, n_zeros: u8) -> FieldElement {
let child_hash = get_hash(SubTree {
leaves: sub_tree.leaves,
height: sub_tree.height + n_zeros,
});
let child_and_path_hash = pedersen_hash(&child_hash, &FieldElement::ZERO);
child_and_path_hash + FieldElement::from(n_zeros)
}

// Hash on both sides: starts with '0' bit and starts with '1' bit.
// Assumes: 0 < partition point < sub_tree.len().
fn get_binary_hash(
sub_tree: SubTree<'_>,
partition_point: usize,
) -> FieldElement {
let zero_hash = get_hash(SubTree {
leaves: &sub_tree.leaves[..partition_point],
height: sub_tree.height + 1,
});
let one_hash = get_hash(SubTree {
leaves: &sub_tree.leaves[partition_point..],
height: sub_tree.height + 1,
});
pedersen_hash(&zero_hash, &one_hash)
}

// Returns the manner the keys of a subtree are splitting: some keys start with '1' or all keys
// start with '0'.
fn get_splitting(sub_tree: &SubTree<'_>) -> SubTreeSplitting {
let mut height = sub_tree.height;

let first_one_bit_index = sub_tree
.leaves
.partition_point(|entry| !entry.key[usize::from(height)]);
if first_one_bit_index < sub_tree.leaves.len() {
return SubTreeSplitting::PartitionPoint(first_one_bit_index);
}

height += 1;
let mut n_zeros = 1;

while height < TREE_HEIGHT {
if sub_tree.leaves.last().expect("sub tree should not be empty").key
[usize::from(height)]
{
break;
}
n_zeros += 1;
height += 1;
}
SubTreeSplitting::CommonZerosPrefix(n_zeros)
}

#[cfg(test)]
mod tests {
use super::*;

// The expected roots were calculated by the starkware-libs/cairo-lang repository. These are the
// roots of PatriciaTree objects with the same leaves.
#[test]
fn test_patricia() {
let root = calculate_root(vec![
FieldElement::ONE,
FieldElement::TWO,
FieldElement::THREE,
]);
let expected_root = FieldElement::from_hex_be(
"0x231e110514ca3a27707cd6c365e00685142d43b03d26f6274db51cbfa91aa1c",
)
.unwrap();
assert_eq!(root, expected_root);
}

#[test]
fn test_edge_patricia() {
let root = calculate_root(vec![FieldElement::ONE]);
let expected_root = FieldElement::from_hex_be(
"0x268a9d47dde48af4b6e2c33932ed1c13adec25555abaa837c376af4ea2f8ad4",
)
.unwrap();
assert_eq!(root, expected_root);
}

#[test]
fn test_binary_patricia() {
let root = calculate_root(vec![FieldElement::ONE, FieldElement::TWO]);
let expected_root = FieldElement::from_hex_be(
"0x599927f1181d5633c6f680dbf039534de49c44e0b9903c5305b2582dfd6a56a",
)
.unwrap();
assert_eq!(root, expected_root);
}
}
Loading

0 comments on commit 2011845

Please sign in to comment.