Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8ef1e59
Initial plan
Copilot Dec 8, 2025
c3b75c7
Add ProgressiveMerkleHasher implementation with basic tests
Copilot Dec 8, 2025
74331be
Add byte buffering and comprehensive tests for ProgressiveMerkleHasher
Copilot Dec 8, 2025
f5ecf94
Export ProgressiveMerkleHasherError from lib.rs
Copilot Dec 8, 2025
3e30512
Add clarifying comment about hash order in progressive merkleization
Copilot Dec 8, 2025
0746652
Refactor ProgressiveMerkleHasher for efficiency - hash chunks as they…
Copilot Dec 8, 2025
16fc4a0
Address code review feedback - improve documentation and extract help…
Copilot Dec 8, 2025
733344a
Refactor ProgressiveMerkleHasher to use MerkleHasher internally for b…
Copilot Dec 9, 2025
8277f30
Fmt
michaelsproul Dec 9, 2025
4fb08f8
Start implementing derive macro
michaelsproul Dec 10, 2025
0a5ed99
Implement TreeHash for ProgressiveBitList
michaelsproul Dec 10, 2025
26fc12f
Remove unnecessary max_leaves from ProgressiveMerkleHasher
michaelsproul Dec 10, 2025
f101c04
mix in length
michaelsproul Dec 11, 2025
f13637a
Add workaround for empty progressive bitlist
michaelsproul Dec 17, 2025
7400510
Use active_fields correctly
michaelsproul Dec 18, 2025
2fd8071
Implement compatible union support with manual selectors
michaelsproul Jan 12, 2026
4f73499
Read/verify ssz attributes as well as tree_hash
michaelsproul Jan 13, 2026
656b85e
Document quirk of bitfield hashing
michaelsproul Jan 13, 2026
9248e51
Remove inelegant temporary hasher
michaelsproul Jan 13, 2026
528a1a7
Remove from_slice
michaelsproul Jan 13, 2026
2f96e24
Document TreeHashType choice
michaelsproul Jan 13, 2026
817387c
Reverse tree order to match spec
macladson Feb 3, 2026
4c9b040
Fix clippy
macladson Feb 3, 2026
858514b
Use git dep instead of path dep
macladson Feb 3, 2026
ab57ec3
Merge branch 'main' into progressive
macladson Feb 3, 2026
ca07459
Fix typo in expect
macladson Feb 5, 2026
5e272f3
General tidy up
macladson Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ readme = "README.md"
repository = "https://github.com/sigp/tree_hash"
keywords = ["ethereum"]
categories = ["cryptography::cryptocurrencies"]

# TODO: Remove once ethereum_ssz is published.
[patch.crates-io]
ethereum_ssz = { git = "https://github.com/sigp/ethereum_ssz", branch = "progressive" }
ethereum_ssz_derive = { git = "https://github.com/sigp/ethereum_ssz", branch = "progressive" }
40 changes: 39 additions & 1 deletion tree_hash/src/impls.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;
use alloy_primitives::{Address, FixedBytes, U128, U256};
use ssz::{Bitfield, Fixed, Variable};
use ssz::{Bitfield, Fixed, Progressive, Variable};
use std::sync::Arc;
use typenum::Unsigned;

Expand Down Expand Up @@ -208,6 +208,44 @@ impl<N: Unsigned + Clone> TreeHash for Bitfield<Variable<N>> {
}
}

impl TreeHash for Bitfield<Progressive> {
fn tree_hash_type() -> TreeHashType {
TreeHashType::List
}

fn tree_hash_packed_encoding(&self) -> PackedEncoding {
unreachable!("ProgressiveBitList should never be packed.")
}

fn tree_hash_packing_factor() -> usize {
unreachable!("ProgressiveBitList should never be packed.")
}

fn tree_hash_root(&self) -> Hash256 {
// XXX: This is a workaround for the fact that the internal representation of bitfields is
// misaligned with the spec. An empty bitlist still stores a single zero byte (see
// `bytes_for_bit_len`), which would otherwise be hashed as a (non-zero) zero chunk rather
// than yielding the empty `merkleize_progressive([]) == Bytes32()` root.
//
// See:
//
// - https://github.com/sigp/ethereum_ssz/pull/68
if self.is_empty() {
return mix_in_length(&Hash256::ZERO, 0);
}

let mut hasher = ProgressiveMerkleHasher::new();
hasher
.write(self.as_slice())
.expect("ProgressiveMerkleHasher has no leaf limit, so write cannot fail");

let bitfield_root = hasher
.finish()
.expect("ProgressiveMerkleHasher has no leaf limit, so finish cannot fail");
mix_in_length(&bitfield_root, self.len())
}
}

impl<N: Unsigned + Clone> TreeHash for Bitfield<Fixed<N>> {
fn tree_hash_type() -> TreeHashType {
TreeHashType::Vector
Expand Down
38 changes: 36 additions & 2 deletions tree_hash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ pub mod impls;
mod merkle_hasher;
mod merkleize_padded;
mod merkleize_standard;
mod progressive_merkle_hasher;

pub use merkle_hasher::{Error, MerkleHasher};
pub use merkleize_padded::merkleize_padded;
pub use merkleize_standard::merkleize_standard;
pub use progressive_merkle_hasher::{
Error as ProgressiveMerkleHasherError, ProgressiveMerkleHasher,
};

use ethereum_hashing::{hash_fixed, ZERO_HASHES, ZERO_HASHES_MAX_INDEX};
use smallvec::SmallVec;
Expand Down Expand Up @@ -61,7 +65,10 @@ pub fn mix_in_length(root: &Hash256, length: usize) -> Hash256 {
let mut length_bytes = [0; BYTES_PER_CHUNK];
length_bytes[0..usize_len].copy_from_slice(&length.to_le_bytes());

Hash256::from_slice(&ethereum_hashing::hash32_concat(root.as_slice(), &length_bytes)[..])
Hash256::from(ethereum_hashing::hash32_concat(
root.as_slice(),
&length_bytes,
))
}

/// Returns `Some(root)` created by hashing `root` and `selector`, if `selector <=
Expand All @@ -86,7 +93,17 @@ pub fn mix_in_selector(root: &Hash256, selector: u8) -> Option<Hash256> {
chunk[0] = selector;

let root = ethereum_hashing::hash32_concat(root.as_slice(), &chunk);
Some(Hash256::from_slice(&root))
Some(Hash256::from(root))
}

/// Returns the node created by hashing `root` and the packed `active_fields` bitvector chunk.
///
/// Used in `TreeHash` for the progressive container type.
pub fn mix_in_active_fields(root: &Hash256, active_fields: [u8; BYTES_PER_CHUNK]) -> Hash256 {
Hash256::from(ethereum_hashing::hash32_concat(
root.as_slice(),
&active_fields,
))
}

/// Returns a cached padding node for a given height.
Expand Down Expand Up @@ -202,4 +219,21 @@ mod test {
&hash[..]
);
}

#[test]
fn mix_active_fields() {
let root = Hash256::from_slice(&[42; BYTES_PER_CHUNK]);
// A multi-bit packed vector (bits 0 and 2 set) so an argument swap or wrong-bytes
// regression would be visible.
let mut active_fields = [0u8; BYTES_PER_CHUNK];
active_fields[0] = 0b0000_0101;

assert_eq!(
mix_in_active_fields(&root, active_fields),
Hash256::from(ethereum_hashing::hash32_concat(
root.as_slice(),
&active_fields
))
);
}
}
13 changes: 13 additions & 0 deletions tree_hash/src/merkle_hasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ pub enum Error {
MaximumLeavesExceeded { max_leaves: usize },
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::MaximumLeavesExceeded { max_leaves } => write!(
f,
"the maximum number of leaves ({max_leaves}) has been exceeded"
),
}
}
}

impl std::error::Error for Error {}

/// Helper struct to store either a hash digest or a slice.
///
/// Should be used as a left or right value for some node.
Expand Down
Loading
Loading