Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
2976d8c
Fix SSZ API
michaelsproul Jul 27, 2026
58c37e8
Merge remote-tracking branch 'origin/main' into progressive
michaelsproul Jul 27, 2026
de9e268
Pin ethereum_ssz
michaelsproul Jul 27, 2026
2bd2335
Merge variant selectors from tree_hash and ssz attributes field-by-field
michaelsproul Jul 27, 2026
58b0279
Give a clear error for enum variants with named fields
michaelsproul Jul 27, 2026
5bfc5c5
Reject 0-variant compatible unions explicitly
michaelsproul Jul 27, 2026
03d9fa4
Reject manual selectors on transparent enums
michaelsproul Jul 27, 2026
8674c20
Add tests for progressive containers and ProgressiveMerkleHasher
eserilev Jul 28, 2026
fb9654a
Merge remote-tracking branch 'origin/main' into progressive-tests
michaelsproul Aug 18, 2026
be57462
Cargo fmt
michaelsproul Aug 18, 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
72 changes: 70 additions & 2 deletions tree_hash/tests/proptests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@
//!
//! - `merkleize_padded` against `merkleize_standard` (the naive reference), and
//! - `MerkleHasher` and `merkle_root` against `merkleize_padded`.
//!
//! `ProgressiveMerkleHasher` and `ProgressiveBitList` hashing are checked against a recursive
//! reference implementation of EIP-7916 `merkleize_progressive`, built on `merkle_root` (itself
//! verified by the chain above).

use ethereum_hashing::hash32_concat;
use proptest::prelude::*;
use ssz::ProgressiveBitList;
use tree_hash::{
merkle_root, merkleize_padded, merkleize_standard, Error, Hash256, MerkleHasher,
BYTES_PER_CHUNK,
merkle_root, merkleize_padded, merkleize_standard, mix_in_length, Error, Hash256, MerkleHasher,
ProgressiveMerkleHasher, TreeHash, BYTES_PER_CHUNK,
};

const MAX_BYTES: usize = 2048;
const MAX_MIN_CHUNKS: usize = 70;
/// Large enough (128 chunks) to cross the progressive level boundaries at 1, 5, 21 and 85 chunks.
const MAX_PROGRESSIVE_BYTES: usize = 4096;
const MAX_BITLIST_BITS: usize = 2048;

/// Computes the root of `bytes` with the naive algorithm, padding the input out to `min_chunks`
/// (rounded to the next power of two) since `merkleize_standard` does not take a chunk count.
Expand Down Expand Up @@ -96,3 +105,62 @@ proptest! {
);
}
}

/// Recursive reference implementation of EIP-7916 `merkleize_progressive` over zero-padded chunks:
///
/// ```text
/// merkleize_progressive([], num_leaves) = Bytes32()
/// merkleize_progressive(chunks, num_leaves) = hash(
/// merkleize(chunks[:num_leaves], num_leaves),
/// merkleize_progressive(chunks[num_leaves:], num_leaves * 4),
/// )
/// ```
fn reference_progressive_root(bytes: &[u8], num_leaves: usize) -> Hash256 {
if bytes.is_empty() {
return Hash256::ZERO;
}
let take = (num_leaves * BYTES_PER_CHUNK).min(bytes.len());
let left = merkle_root(&bytes[..take], num_leaves);
let right = reference_progressive_root(&bytes[take..], num_leaves * 4);
Hash256::from(hash32_concat(left.as_slice(), right.as_slice()))
}

proptest! {
#[test]
fn progressive_hasher_matches_reference(
bytes in proptest::collection::vec(any::<u8>(), 0..=MAX_PROGRESSIVE_BYTES),
write_size in 1_usize..=64,
) {
// Random `write_size` exercises the partial-chunk carry buffer across write boundaries.
let mut hasher = ProgressiveMerkleHasher::new();
for chunk in bytes.chunks(write_size) {
hasher.write(chunk).expect("progressive hasher has no leaf limit");
}
let root = hasher.finish().expect("progressive hasher has no leaf limit");

prop_assert_eq!(root, reference_progressive_root(&bytes, 1));
}

#[test]
fn progressive_bitlist_matches_reference(
bits in proptest::collection::vec(any::<bool>(), 0..=MAX_BITLIST_BITS),
) {
let mut bitlist = ProgressiveBitList::with_capacity(bits.len());
for (i, bit) in bits.iter().enumerate() {
bitlist.set(i, *bit).expect("index is within the bitlist length");
}

// Pack the bits independently of the `Bitfield` internals. In particular an empty bitlist
// packs to zero bytes here, whereas `Bitfield` stores a single zero byte internally, so
// this catches any regression of the empty-list workaround in `tree_hash_root`.
let mut packed = vec![0u8; bits.len().div_ceil(8)];
for (i, bit) in bits.iter().enumerate() {
if *bit {
packed[i / 8] |= 1 << (i % 8);
}
}
let expected = mix_in_length(&reference_progressive_root(&packed, 1), bits.len());

prop_assert_eq!(bitlist.tree_hash_root(), expected);
}
}
209 changes: 209 additions & 0 deletions tree_hash/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,212 @@ fn progressive_bitlist_nonempty_all_false_differs_from_empty() {
tree_hash::mix_in_length(&progressive_merkle_root_bytes(bitlist.as_slice()), 8)
);
}

#[derive(TreeHash)]
#[tree_hash(
struct_behaviour = "progressive_container",
active_fields(1, 1, 1, 1, 1, 1)
)]
struct ProgressiveContainerSixFields {
a: u8,
b: u8,
c: u8,
d: u8,
e: u8,
f: u8,
}

#[test]
fn progressive_container_crosses_level_boundary() {
// Six leaves span three progressive levels (1 + 4 + partial 16), so this exercises the
// macro-generated write loop across level boundaries, unlike the smaller containers above.
let container = ProgressiveContainerSixFields {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6,
};

let leaves = [
container.a.tree_hash_root(),
container.b.tree_hash_root(),
container.c.tree_hash_root(),
container.d.tree_hash_root(),
container.e.tree_hash_root(),
container.f.tree_hash_root(),
];
let expected = mix_in_active_fields(
&progressive_merkle_root(&leaves),
packed_active_fields(&[true; 6]),
);
assert_eq!(container.tree_hash_root(), expected);

// Independently computed from the EIP-7916/EIP-7495 pseudocode (not using this crate).
assert_eq!(
container.tree_hash_root(),
Hash256::from_str("0x5b3a785823a6af49e9add04543130b578f8ad774048632aec9d765f0eb949eab")
.unwrap()
);
}

#[derive(TreeHash)]
#[tree_hash(
struct_behaviour = "progressive_container",
active_fields(1, 0, 0, 0, 0, 0, 0, 0, 0, 1)
)]
struct ProgressiveContainerMultiByteActiveFields {
a: u8,
b: u8,
}

#[test]
fn progressive_container_multi_byte_active_fields() {
// Ten positions make the packed `active_fields` bitvector span two bytes, exercising the
// compile-time packing end-to-end through a derived impl (not just the `attrs` unit tests).
let container = ProgressiveContainerMultiByteActiveFields { a: 0xaa, b: 0xbb };

let mut leaves = vec![container.a.tree_hash_root()];
leaves.extend([Hash256::ZERO; 8]);
leaves.push(container.b.tree_hash_root());

let mut bits = [false; 10];
bits[0] = true;
bits[9] = true;
let expected = mix_in_active_fields(
&progressive_merkle_root(&leaves),
packed_active_fields(&bits),
);
assert_eq!(container.tree_hash_root(), expected);

// Independently computed from the EIP-7916/EIP-7495 pseudocode (not using this crate).
assert_eq!(
container.tree_hash_root(),
Hash256::from_str("0x346cfa442bcc5f2288316f185aa5b7e0a292d2e287862cc6f177ee1362862148")
.unwrap()
);
}

#[derive(TreeHash)]
#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1))]
struct ProgressiveContainerWithSkip {
a: u8,
// Never read; only present to prove `skip_hashing` excludes it from the root and does not
// consume an `active_fields` entry.
#[allow(dead_code)]
#[tree_hash(skip_hashing)]
b: u64,
c: u8,
}

#[test]
fn progressive_container_skip_hashing() {
let x = ProgressiveContainerWithSkip { a: 1, b: 999, c: 3 };

// The skipped field is absent from the tree: only `a` and `c` are leaves, matching the two
// `active_fields` entries.
let leaves = [x.a.tree_hash_root(), x.c.tree_hash_root()];
let expected = mix_in_active_fields(
&progressive_merkle_root(&leaves),
packed_active_fields(&[true, true]),
);
assert_eq!(x.tree_hash_root(), expected);

// Mutating only the skipped field must not change the root.
let y = ProgressiveContainerWithSkip {
a: 1,
b: 12345,
c: 3,
};
assert_eq!(x.tree_hash_root(), y.tree_hash_root());
}

#[derive(TreeHash)]
#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1))]
struct ProgressiveContainerNested {
inner: ProgressiveContainerThreeFields,
tag: u8,
}

#[test]
fn progressive_container_nested() {
// A progressive container nested inside another progressive container contributes its own
// root (including its `active_fields` mix-in) as a single leaf.
let x = ProgressiveContainerNested {
inner: ProgressiveContainerThreeFields {
a: 7,
b: 8,
c: Hash256::repeat_byte(9),
},
tag: 5,
};

let leaves = [x.inner.tree_hash_root(), x.tag.tree_hash_root()];
let expected = mix_in_active_fields(
&progressive_merkle_root(&leaves),
packed_active_fields(&[true, true]),
);
assert_eq!(x.tree_hash_root(), expected);
}

#[derive(TreeHash)]
struct PlainContainerWrapsProgressive {
inner: ProgressiveContainerOneField,
tag: u8,
}

#[test]
fn plain_container_wraps_progressive() {
// The other direction: a progressive container as a field of an ordinary container.
let x = PlainContainerWrapsProgressive {
inner: ProgressiveContainerOneField { x: 125 },
tag: 5,
};
assert_eq!(
x.tree_hash_root(),
container_root(&[x.inner.tree_hash_root(), x.tag.tree_hash_root()])
);
}

#[derive(TreeHash)]
#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1))]
struct ProgressiveGeneric<T: TreeHash> {
value: T,
count: u64,
}

#[test]
fn progressive_container_generic() {
let x = ProgressiveGeneric {
value: 42u16,
count: 7,
};
let leaves = [x.value.tree_hash_root(), x.count.tree_hash_root()];
let expected = mix_in_active_fields(
&progressive_merkle_root(&leaves),
packed_active_fields(&[true, true]),
);
assert_eq!(x.tree_hash_root(), expected);
}

#[test]
fn progressive_container_single_field_differs_from_plain() {
// Unlike an ordinary single-field container (whose root equals the field's root), a
// single-field progressive container wraps the leaf in the progressive structure and the
// `active_fields` mix-in, so the roots must differ.
let container = ProgressiveContainerOneField { x: 125 };
assert_ne!(container.tree_hash_root(), container.x.tree_hash_root());
}

#[test]
fn progressive_container_and_compatible_union_are_container_type() {
assert_eq!(
ProgressiveContainerThreeFields::tree_hash_type(),
tree_hash::TreeHashType::Container
);
assert_eq!(
CompatUnion::tree_hash_type(),
tree_hash::TreeHashType::Container
);
}
Loading