diff --git a/Cargo.toml b/Cargo.toml index 9baf80f..eb43e66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/tree_hash/src/impls.rs b/tree_hash/src/impls.rs index bc9cd59..0990b31 100644 --- a/tree_hash/src/impls.rs +++ b/tree_hash/src/impls.rs @@ -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; @@ -208,6 +208,44 @@ impl TreeHash for Bitfield> { } } +impl TreeHash for Bitfield { + 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 TreeHash for Bitfield> { fn tree_hash_type() -> TreeHashType { TreeHashType::Vector diff --git a/tree_hash/src/lib.rs b/tree_hash/src/lib.rs index e5ac66f..3d53272 100644 --- a/tree_hash/src/lib.rs +++ b/tree_hash/src/lib.rs @@ -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; @@ -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(ðereum_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 <= @@ -86,7 +93,17 @@ pub fn mix_in_selector(root: &Hash256, selector: u8) -> Option { 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. @@ -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 + )) + ); + } } diff --git a/tree_hash/src/merkle_hasher.rs b/tree_hash/src/merkle_hasher.rs index 07d4352..526c78d 100644 --- a/tree_hash/src/merkle_hasher.rs +++ b/tree_hash/src/merkle_hasher.rs @@ -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. diff --git a/tree_hash/src/progressive_merkle_hasher.rs b/tree_hash/src/progressive_merkle_hasher.rs new file mode 100644 index 0000000..3e8208f --- /dev/null +++ b/tree_hash/src/progressive_merkle_hasher.rs @@ -0,0 +1,471 @@ +use crate::{Hash256, MerkleHasher, BYTES_PER_CHUNK}; +use ethereum_hashing::hash32_concat; +use smallvec::SmallVec; + +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + MerkleHasher(crate::merkle_hasher::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::MerkleHasher(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::MerkleHasher(e) => Some(e), + } + } +} + +/// A progressive Merkle hasher that implements the semantics of `merkleize_progressive` as +/// defined in EIP-7916. +/// +/// The progressive merkle tree has a unique structure where: +/// - At each level, the left child is a binary merkle tree with a specific number of leaves +/// - The right child recursively contains more progressive structure +/// - The number of leaves in each left subtree grows by 4x at each level (1, 4, 16, 64, ...) +/// +/// # Example Tree Structure +/// +/// ```text +/// root +/// /\ +/// / \ +/// 1: chunks[0 ..< 1] /\ +/// / \ +/// 4: chunks[1 ..< 5] /\ +/// / \ +/// 16: chunks[5 ..< 21] /\ +/// / \ +/// 64: chunks[21 ..< 85] 0 +/// ``` +/// +/// This structure allows efficient appending and proof generation for growing lists. +/// +/// # Efficiency +/// +/// This implementation hashes chunks as they are streamed in, storing only the minimum +/// necessary state (completed subtree roots). When a level is filled, its binary merkle +/// root is computed and stored, avoiding the need to keep all chunks in memory. +pub struct ProgressiveMerkleHasher { + /// Completed subtree roots at each level, stored in order of completion. + /// Index 0 = first completed level (1 leaf), index 1 = second level (4 leaves), etc. + /// Level i contains 4^i leaves. + completed_roots: Vec, + /// `MerkleHasher` for computing the current level's binary tree root, created lazily on the + /// first chunk of each level so that a level completed by its final chunk does not allocate a + /// hasher that is never used. + current_hasher: Option, + /// The number of leaves expected at the current level (1, 4, 16, 64, ...). + current_level_size: usize, + /// Number of chunks written to the current hasher. + current_level_chunks: usize, + /// Carry for bytes that haven't been completed into a chunk yet. Always < 32 bytes, so this + /// never spills to the heap. + buffer: SmallVec<[u8; BYTES_PER_CHUNK]>, +} + +impl ProgressiveMerkleHasher { + /// Create a new progressive merkle hasher that can accept any number of chunks. + pub fn new() -> Self { + Self { + completed_roots: Vec::new(), + current_hasher: None, + current_level_size: 1, + current_level_chunks: 0, + buffer: SmallVec::new(), + } + } + + /// Write bytes to the hasher. + /// + /// The bytes will be split into 32-byte chunks. Bytes are buffered across multiple + /// write calls to ensure proper chunk boundaries. Complete subtrees are hashed + /// immediately as chunks are written. + /// + /// # Errors + /// + /// This hasher imposes no leaf limit, so under normal use `write` does not fail; the `Result` + /// only forwards an error from the internal per-level [`MerkleHasher`], which is not expected + /// to occur. + pub fn write(&mut self, bytes: &[u8]) -> Result<(), Error> { + let mut bytes = bytes; + + // If a partial carry exists, top it up from the front of `bytes` to complete a chunk. + if !self.buffer.is_empty() { + let take = (BYTES_PER_CHUNK - self.buffer.len()).min(bytes.len()); + self.buffer.extend_from_slice(&bytes[..take]); + bytes = &bytes[take..]; + + // Still short of a full chunk; nothing more to do. + if self.buffer.len() < BYTES_PER_CHUNK { + return Ok(()); + } + + let chunk = std::mem::take(&mut self.buffer); + self.process_chunk(&chunk)?; + } + + // Forward each complete 32-byte chunk directly, then carry the remainder. + let mut chunks = bytes.chunks_exact(BYTES_PER_CHUNK); + for chunk in &mut chunks { + self.process_chunk(chunk)?; + } + self.buffer.extend_from_slice(chunks.remainder()); + + Ok(()) + } + + /// Process a single 32-byte chunk by adding it to the current level and completing the level if + /// full. + fn process_chunk(&mut self, chunk: &[u8]) -> Result<(), Error> { + // Lazily create the current level's hasher, so a level completed by its final chunk does + // not allocate a hasher that is immediately discarded. + let level_size = self.current_level_size; + let hasher = self + .current_hasher + .get_or_insert_with(|| MerkleHasher::with_leaves(level_size)); + hasher.write(chunk).map_err(Error::MerkleHasher)?; + + self.current_level_chunks += 1; + + // Once the current level is full, finalize its binary root and advance to the next level + // (4x larger). `current_level_size` grows as 4^i, which cannot overflow `usize` in + // practice: completing level i requires streaming 4^i chunks, far beyond any realizable + // input. + if self.current_level_chunks == self.current_level_size { + let completed_hasher = self + .current_hasher + .take() + .expect("current hasher exists after a chunk has been written"); + let root = completed_hasher.finish().map_err(Error::MerkleHasher)?; + self.completed_roots.push(root); + + self.current_level_size *= 4; + self.current_level_chunks = 0; + } + + Ok(()) + } + + /// Finish the hasher and return the progressive merkle root. + /// + /// This completes any partial level and combines all completed subtree roots + /// according to the progressive merkleization algorithm. + /// + /// Any remaining bytes in the buffer will be padded to form a final chunk. + pub fn finish(mut self) -> Result { + // Process any remaining bytes in the buffer as a final chunk + if !self.buffer.is_empty() { + let mut chunk = [0u8; BYTES_PER_CHUNK]; + chunk[..self.buffer.len()].copy_from_slice(&self.buffer); + self.process_chunk(&chunk)?; + } + + // With no chunks at all, the root is zero. Because the first level holds a single chunk, + // any processed chunk immediately populates `completed_roots`, so this is exactly the + // empty case. + if self.completed_roots.is_empty() && self.current_level_chunks == 0 { + return Ok(Hash256::ZERO); + } + + // If the final level is partially filled, compute its (zero-padded) binary root. + let current_root = if self.current_level_chunks > 0 { + let hasher = self + .current_hasher + .take() + .expect("current hasher exists while the level is partially filled"); + Some(hasher.finish().map_err(Error::MerkleHasher)?) + } else { + None + }; + + // completed_roots are in order [smallest level, ..., largest level]; build the progressive + // tree by folding them from the largest level inward. + Ok(build_progressive_root(current_root, &self.completed_roots)) + } +} + +/// Build the final progressive merkle root by combining completed subtree roots. +/// +/// The progressive tree structure: at each node, `hash(left = this level, right = deeper levels)`. +/// This builds the tree from the largest (rightmost) level backwards to the smallest (leftmost). +fn build_progressive_root(current_root: Option, completed_roots: &[Hash256]) -> Hash256 { + // Per EIP-7916, a partial final level still follows the progressive structure: + // merkleize_progressive(chunks, n) + // = hash(merkleize(chunks[:n], n), merkleize_progressive(chunks[n:], n*4)) + // so a partial level with k chunks becomes hash(merkleize(chunks, n), ZERO). + let mut result = if let Some(curr) = current_root { + Hash256::from(hash32_concat(curr.as_slice(), Hash256::ZERO.as_slice())) + } else { + Hash256::ZERO + }; + + // Fold completed roots from largest to smallest: result = hash(completed_root, result), where + // completed_root is the left (binary) subtree and result accumulates the deeper levels. + for &completed_root in completed_roots.iter().rev() { + result = Hash256::from(hash32_concat(completed_root.as_slice(), result.as_slice())); + } + + result +} + +impl Default for ProgressiveMerkleHasher { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::merkle_root; + + /// Independent, non-streaming reference implementation of `merkleize_progressive` (EIP-7916), + /// used to cross-check the streaming hasher. + fn reference_progressive(chunks: &[[u8; BYTES_PER_CHUNK]], num_leaves: usize) -> Hash256 { + if chunks.is_empty() { + return Hash256::ZERO; + } + let take = num_leaves.min(chunks.len()); + let left_bytes: Vec = chunks[..take].iter().flatten().copied().collect(); + let left = merkle_root(&left_bytes, num_leaves); + let right = reference_progressive(&chunks[take..], num_leaves * 4); + Hash256::from(hash32_concat(left.as_slice(), right.as_slice())) + } + + fn streaming_root(chunks: &[[u8; BYTES_PER_CHUNK]]) -> Hash256 { + let mut hasher = ProgressiveMerkleHasher::new(); + for chunk in chunks { + hasher.write(chunk).unwrap(); + } + hasher.finish().unwrap() + } + + fn indexed_chunk(i: usize) -> [u8; BYTES_PER_CHUNK] { + let mut c = [0u8; BYTES_PER_CHUNK]; + c[..8].copy_from_slice(&(i as u64 + 1).to_le_bytes()); + c + } + + /// Cross-check the streaming hasher against the recursive reference across a range of chunk + /// counts, including exact level fills (1, 5, 21, 85, 341), partial trailing levels, and a + /// deep level (>256) that crosses the inner `MerkleHasher`'s inline-stack capacity. + #[test] + fn matches_reference_oracle() { + for count in [ + 0usize, 1, 2, 3, 4, 5, 6, 16, 20, 21, 22, 64, 84, 85, 86, 200, 341, 342, + ] { + let chunks: Vec<[u8; BYTES_PER_CHUNK]> = (0..count).map(indexed_chunk).collect(); + assert_eq!( + streaming_root(&chunks), + reference_progressive(&chunks, 1), + "mismatch for {count} chunks" + ); + } + } + + #[test] + fn test_empty_tree() { + let hasher = ProgressiveMerkleHasher::new(); + let root = hasher.finish().unwrap(); + assert_eq!(root, Hash256::ZERO); + } + + #[test] + fn test_single_chunk() { + let mut hasher = ProgressiveMerkleHasher::new(); + let chunk = [1u8; BYTES_PER_CHUNK]; + hasher.write(&chunk).unwrap(); + let root = hasher.finish().unwrap(); + + // For a single chunk, the progressive tree should be: + // hash(merkleize([chunk], 1), merkleize_progressive([], 4)) + // = hash(chunk, zero_hash) + let left = Hash256::from_slice(&chunk); + let zero_right = Hash256::ZERO; + let expected = Hash256::from_slice(&hash32_concat(left.as_slice(), zero_right.as_slice())); + + assert_eq!(root, expected); + } + + #[test] + fn test_two_chunks() { + let mut hasher = ProgressiveMerkleHasher::new(); + let chunk1 = [1u8; BYTES_PER_CHUNK]; + let chunk2 = [2u8; BYTES_PER_CHUNK]; + hasher.write(&chunk1).unwrap(); + hasher.write(&chunk2).unwrap(); + let root = hasher.finish().unwrap(); + + // First chunk goes to left (num_leaves=1) + // Second chunk goes to right recursive call (num_leaves=4) + + // Left: binary tree with 1 leaf = chunk1 + let left = Hash256::from_slice(&chunk1); + + // Right: progressive tree with chunk2 at num_leaves=4 + // At this level: hash(merkleize([chunk2], 4), merkleize_progressive([], 16)) + // = hash(merkle([chunk2], 4), zero_hash) + let chunk2_padded = merkle_root(&chunk2, 4); + let zero_right_inner = Hash256::ZERO; + let right = Hash256::from_slice(&hash32_concat( + chunk2_padded.as_slice(), + zero_right_inner.as_slice(), + )); + + let expected = Hash256::from_slice(&hash32_concat(left.as_slice(), right.as_slice())); + assert_eq!(root, expected); + } + + #[test] + fn test_partial_chunk() { + let mut hasher = ProgressiveMerkleHasher::new(); + let partial = vec![1u8, 2u8, 3u8]; + hasher.write(&partial).unwrap(); + let root = hasher.finish().unwrap(); + + // Partial chunk should be padded with zeros + let mut chunk = [0u8; BYTES_PER_CHUNK]; + chunk[0] = 1; + chunk[1] = 2; + chunk[2] = 3; + + let left = Hash256::from_slice(&chunk); + let zero_right = Hash256::ZERO; + let expected = Hash256::from_slice(&hash32_concat(left.as_slice(), zero_right.as_slice())); + + assert_eq!(root, expected); + } + + #[test] + fn test_multiple_writes() { + let mut hasher = ProgressiveMerkleHasher::new(); + hasher.write(&[1u8; 16]).unwrap(); + hasher.write(&[2u8; 16]).unwrap(); + hasher.write(&[3u8; 32]).unwrap(); + let root = hasher.finish().unwrap(); + + // The three writes form exactly two chunks: [1; 16] ++ [2; 16], then [3; 32]. + let mut chunk0 = [0u8; BYTES_PER_CHUNK]; + chunk0[..16].fill(1); + chunk0[16..].fill(2); + let chunk1 = [3u8; BYTES_PER_CHUNK]; + assert_eq!(root, reference_progressive(&[chunk0, chunk1], 1)); + } + + #[test] + fn test_five_chunks() { + // Test with 5 chunks as per the problem statement structure: + // chunks[0] goes to left at level 1 (1 leaf) + // chunks[1..5] go to right recursive call (4 leaves at level 2) + let mut hasher = ProgressiveMerkleHasher::new(); + for i in 0..5 { + let mut chunk = [0u8; BYTES_PER_CHUNK]; + chunk[0] = i as u8; + hasher.write(&chunk).unwrap(); + } + let root = hasher.finish().unwrap(); + + // Manually compute expected root: + // Left: chunks[0] + let mut chunk0 = [0u8; BYTES_PER_CHUNK]; + chunk0[0] = 0; + let left = Hash256::from_slice(&chunk0); + + // Right: merkleize_progressive(chunks[1..5], 4) + // Which is: hash(merkleize(chunks[1..5], 4), merkleize_progressive([], 16)) + let chunks_1_to_4: Vec = (1..5) + .flat_map(|i| { + let mut chunk = [0u8; BYTES_PER_CHUNK]; + chunk[0] = i; + chunk + }) + .collect(); + let left_inner = merkle_root(&chunks_1_to_4, 4); + let right_inner = Hash256::ZERO; + let right = Hash256::from_slice(&hash32_concat( + left_inner.as_slice(), + right_inner.as_slice(), + )); + + let expected = Hash256::from_slice(&hash32_concat(left.as_slice(), right.as_slice())); + assert_eq!(root, expected); + } + + #[test] + fn test_21_chunks() { + // 21 chunks exactly fills levels 1 + 4 + 16. + let chunks: Vec<[u8; BYTES_PER_CHUNK]> = (0..21).map(indexed_chunk).collect(); + assert_eq!(streaming_root(&chunks), reference_progressive(&chunks, 1)); + } + + #[test] + fn test_85_chunks() { + // 85 chunks exactly fills levels 1 + 4 + 16 + 64. + let chunks: Vec<[u8; BYTES_PER_CHUNK]> = (0..85).map(indexed_chunk).collect(); + assert_eq!(streaming_root(&chunks), reference_progressive(&chunks, 1)); + } + + #[test] + fn test_consistency_across_write_patterns() { + // Test that different write patterns produce the same result + let chunks: Vec<[u8; BYTES_PER_CHUNK]> = (0..10) + .map(|i| { + let mut chunk = [0u8; BYTES_PER_CHUNK]; + chunk[0] = i; + chunk + }) + .collect(); + + // Write all chunks individually + let mut hasher1 = ProgressiveMerkleHasher::new(); + for chunk in &chunks { + hasher1.write(chunk).unwrap(); + } + let root1 = hasher1.finish().unwrap(); + + // Write all chunks at once + let mut hasher2 = ProgressiveMerkleHasher::new(); + let all_bytes: Vec = chunks.iter().flat_map(|c| c.iter().copied()).collect(); + hasher2.write(&all_bytes).unwrap(); + let root2 = hasher2.finish().unwrap(); + + // Write in groups + let mut hasher3 = ProgressiveMerkleHasher::new(); + hasher3.write(&all_bytes[..3 * BYTES_PER_CHUNK]).unwrap(); + hasher3 + .write(&all_bytes[3 * BYTES_PER_CHUNK..7 * BYTES_PER_CHUNK]) + .unwrap(); + hasher3.write(&all_bytes[7 * BYTES_PER_CHUNK..]).unwrap(); + let root3 = hasher3.finish().unwrap(); + + assert_eq!(root1, root2); + assert_eq!(root1, root3); + } + + #[test] + fn test_byte_streaming() { + // Test that we can write bytes in various chunk sizes + let data = vec![42u8; BYTES_PER_CHUNK * 3 + 10]; + + // Write all at once + let mut hasher1 = ProgressiveMerkleHasher::new(); + hasher1.write(&data).unwrap(); + let root1 = hasher1.finish().unwrap(); + + // Write in smaller chunks + let mut hasher2 = ProgressiveMerkleHasher::new(); + hasher2.write(&data[0..50]).unwrap(); + hasher2.write(&data[50..]).unwrap(); + let root2 = hasher2.finish().unwrap(); + + assert_eq!(root1, root2); + } +} diff --git a/tree_hash/tests/tests.rs b/tree_hash/tests/tests.rs index 8531548..9ec63f9 100644 --- a/tree_hash/tests/tests.rs +++ b/tree_hash/tests/tests.rs @@ -1,6 +1,11 @@ use alloy_primitives::{Address, U128, U160, U256}; +use ssz::ProgressiveBitList; use ssz_derive::Encode; -use tree_hash::{Hash256, MerkleHasher, PackedEncoding, TreeHash, BYTES_PER_CHUNK}; +use std::str::FromStr; +use tree_hash::{ + mix_in_active_fields, Hash256, MerkleHasher, PackedEncoding, ProgressiveMerkleHasher, TreeHash, + BYTES_PER_CHUNK, +}; use tree_hash_derive::TreeHash; #[derive(Encode)] @@ -167,3 +172,186 @@ fn packed_encoding_example() { ); } } + +#[derive(TreeHash)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1))] +struct ProgressiveContainerOneField { + x: u8, +} + +#[test] +fn progressive_container_one_field() { + let container = ProgressiveContainerOneField { x: 125 }; + assert_eq!( + container.tree_hash_root(), + Hash256::from_str("0xb6a2f148c33179dec1bdaa979a11776ff2d881fca93974b286443a8539dc0872") + .unwrap() + ); +} + +/// Reference merkleization of a progressive container's leaves, via the public hasher. +fn progressive_merkle_root(leaves: &[Hash256]) -> Hash256 { + let mut hasher = ProgressiveMerkleHasher::new(); + for leaf in leaves { + hasher.write(leaf.as_slice()).unwrap(); + } + hasher.finish().unwrap() +} + +/// Pack `active_fields` bits LSB-first into a single 32-byte chunk. +fn packed_active_fields(bits: &[bool]) -> [u8; BYTES_PER_CHUNK] { + let mut packed = [0u8; BYTES_PER_CHUNK]; + for (i, bit) in bits.iter().enumerate() { + if *bit { + packed[i / 8] |= 1 << (i % 8); + } + } + packed +} + +#[derive(TreeHash)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1, 1))] +struct ProgressiveContainerThreeFields { + a: u8, + b: u64, + c: Hash256, +} + +#[test] +fn progressive_container_multi_field() { + let container = ProgressiveContainerThreeFields { + a: 7, + b: 0x0102_0304_0506_0708, + c: Hash256::repeat_byte(0xcd), + }; + + let leaves = [ + container.a.tree_hash_root(), + container.b.tree_hash_root(), + container.c.tree_hash_root(), + ]; + let expected = mix_in_active_fields( + &progressive_merkle_root(&leaves), + packed_active_fields(&[true, true, true]), + ); + + assert_eq!(container.tree_hash_root(), expected); +} + +#[derive(TreeHash)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 0, 1))] +struct ProgressiveContainerWithGap { + a: u8, + c: u16, +} + +#[test] +fn progressive_container_inactive_field() { + let container = ProgressiveContainerWithGap { a: 9, c: 0xbeef }; + + // The inactive middle position must contribute a zero leaf so chunk positions stay stable. + let leaves = [ + container.a.tree_hash_root(), + Hash256::ZERO, + container.c.tree_hash_root(), + ]; + let expected = mix_in_active_fields( + &progressive_merkle_root(&leaves), + packed_active_fields(&[true, false, true]), + ); + + assert_eq!(container.tree_hash_root(), expected); +} + +#[derive(TreeHash)] +#[tree_hash(enum_behaviour = "compatible_union")] +enum CompatUnion { + #[tree_hash(selector = "1")] + A(u8), + #[tree_hash(selector = "4")] + B(HashVec), +} + +#[test] +fn compatible_union() { + // The root is `mix_in_selector(inner_root, selector)` using the explicit per-variant selectors. + assert_eq!( + CompatUnion::A(2).tree_hash_root(), + mix_in_selector(u8_hash(2), 1) + ); + assert_eq!( + CompatUnion::B(HashVec::from(vec![2])).tree_hash_root(), + mix_in_selector(u8_hash_concat(2, 1), 4) + ); +} + +#[derive(Encode, TreeHash)] +#[ssz(enum_behaviour = "compatible_union")] +#[tree_hash(enum_behaviour = "compatible_union")] +enum CompatUnionSszSelectors { + #[ssz(selector = "3")] + A(u8), + #[ssz(selector = "5")] + B(u8), +} + +#[test] +fn compatible_union_reads_ssz_selectors() { + // These variants carry no `tree_hash(selector)`, so `tree_hash` must read the selectors from + // the sibling `ssz` attribute (the `(None, Some(ssz))` fallback in `parse_variant_opts`). + assert_eq!( + CompatUnionSszSelectors::A(2).tree_hash_root(), + mix_in_selector(u8_hash(2), 3) + ); + assert_eq!( + CompatUnionSszSelectors::B(2).tree_hash_root(), + mix_in_selector(u8_hash(2), 5) + ); +} + +/// Merkleize raw bytes (zero-padded into chunks) via the public progressive hasher. +fn progressive_merkle_root_bytes(bytes: &[u8]) -> Hash256 { + let mut hasher = ProgressiveMerkleHasher::new(); + hasher.write(bytes).unwrap(); + hasher.finish().unwrap() +} + +#[test] +fn progressive_bitlist_empty() { + // An empty bitlist must hash as mix_in_length(merkleize_progressive([]) = ZERO, 0), exercising + // the empty-list workaround (an empty bitlist still stores a single zero byte internally). + let bitlist = ProgressiveBitList::with_capacity(0).unwrap(); + assert_eq!( + bitlist.tree_hash_root(), + tree_hash::mix_in_length(&Hash256::ZERO, 0) + ); +} + +#[test] +fn progressive_bitlist_matches_packed_progressive() { + // The root must equal mix_in_length(merkleize_progressive(pack_bits(value)), len) for a range + // of lengths, including byte- and chunk-aligned cases and progressive level crossings. + for len in [1usize, 7, 8, 9, 16, 100, 256, 257, 1024] { + let mut bitlist = ProgressiveBitList::with_capacity(len).unwrap(); + for i in (0..len).step_by(3) { + bitlist.set(i, true).unwrap(); + } + + let expected = + tree_hash::mix_in_length(&progressive_merkle_root_bytes(bitlist.as_slice()), len); + assert_eq!(bitlist.tree_hash_root(), expected, "len = {len}"); + } +} + +#[test] +fn progressive_bitlist_nonempty_all_false_differs_from_empty() { + // A non-empty all-false bitlist must NOT short-circuit to the empty value. + let bitlist = ProgressiveBitList::with_capacity(8).unwrap(); + let empty = ProgressiveBitList::with_capacity(0).unwrap(); + + assert_ne!(bitlist.tree_hash_root(), empty.tree_hash_root()); + assert_eq!( + bitlist.tree_hash_root(), + tree_hash::mix_in_length(&progressive_merkle_root_bytes(bitlist.as_slice()), 8) + ); +} diff --git a/tree_hash_derive/src/attrs.rs b/tree_hash_derive/src/attrs.rs new file mode 100644 index 0000000..e37c78c --- /dev/null +++ b/tree_hash_derive/src/attrs.rs @@ -0,0 +1,244 @@ +use darling::{ast::NestedMeta, Error, FromDeriveInput, FromMeta}; +use quote::quote; + +pub const MAX_ACTIVE_FIELDS: usize = 256; +/// The number of bytes in the packed `active_fields` bitvector. +/// +/// `active_fields` is restricted to `MAX_ACTIVE_FIELDS` (256) bits so that it packs into exactly +/// one 32-byte chunk, which must equal `tree_hash::BYTES_PER_CHUNK` (the generated code asserts +/// this at compile time). +pub const ACTIVE_FIELDS_PACKED_BYTES_LEN: usize = MAX_ACTIVE_FIELDS / 8; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(tree_hash))] +pub struct StructOpts { + #[darling(default)] + pub enum_behaviour: Option, + #[darling(default)] + pub struct_behaviour: Option, + #[darling(default)] + pub active_fields: Option, +} + +/// Variant-level configuration (for enums). +/// +/// These attributes NEED to be kept in sync with `ethereum_ssz` because both crates try to read +/// each others attributes to avoid mandatory duplication. In future this might mean parsing some +/// SSZ-only attributes here and then ignoring them. +#[derive(Debug, Default, PartialEq, FromMeta)] +// `allow_unknown_fields` ensures that `ssz`-only keys (current or future) on a shared variant +// attribute are tolerated and ignored here, rather than causing a parse error. This mirrors +// `ssz_derive`'s `VariantOpts`, which tolerates `tree_hash`-only keys for the same reason. +#[darling(allow_unknown_fields)] +pub struct VariantOpts { + #[darling(default)] + pub selector: Option, +} + +#[derive(Debug, FromMeta)] +pub enum EnumBehaviour { + Transparent, + Union, + CompatibleUnion, +} + +#[derive(Debug, Default, FromMeta)] +pub enum StructBehaviour { + #[default] + Container, + ProgressiveContainer, +} + +#[derive(Debug)] +pub struct ActiveFields { + pub active_fields: Vec, +} + +impl FromMeta for ActiveFields { + fn from_list(items: &[NestedMeta]) -> Result { + let active_fields = items + .iter() + .map(|nested_meta| match u8::from_nested_meta(nested_meta) { + Ok(0) => Ok(false), + Ok(1) => Ok(true), + Ok(n) => Err(Error::custom(format!( + "invalid integer in active_fields: {n}" + ))), + Err(e) => Err(Error::custom(format!( + "unable to parse active_fields entry: {e:?}" + ))), + }) + .collect::>()?; + Self::new(active_fields) + } +} + +impl ActiveFields { + fn new(active_fields: Vec) -> Result { + if active_fields.is_empty() { + return Err(Error::custom("active_fields must be non-empty".to_string())); + } + if active_fields.len() > MAX_ACTIVE_FIELDS { + return Err(Error::custom(format!( + "active_fields cannot contain more than {MAX_ACTIVE_FIELDS} entries" + ))); + } + + // A trailing inactive field is materialized as a real zero leaf, so it would change the + // root rather than being a no-op. The progressive container's `active_fields` bitvector is + // canonically delimited by its highest active field, so a trailing `0` is not permitted. + if let Some(false) = active_fields.last() { + return Err(Error::custom( + "the last entry of active_fields must not be 0 (the bitvector is delimited by the \ + highest active field)" + .to_string(), + )); + } + + Ok(Self { active_fields }) + } + + pub fn packed(&self) -> [u8; ACTIVE_FIELDS_PACKED_BYTES_LEN] { + let mut result = [0; ACTIVE_FIELDS_PACKED_BYTES_LEN]; + for (i, bit) in self.active_fields.iter().enumerate() { + if *bit { + result[i / 8] |= 1 << (i % 8); + } + } + result + } + + /// Return tokens for the packed representation of these `active_fields`. + /// + /// We compute the packed representation at compile-time, and then inline it via the output + /// of this function. + pub fn packed_tokens(&self) -> proc_macro2::TokenStream { + let packed = self.packed(); + let bytes = packed.iter(); + quote! { + [ + #(#bytes),* + ] + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn active_fields_packed_basic() { + let active_fields = ActiveFields { + active_fields: vec![true], + }; + assert_eq!( + active_fields.packed(), + [ + 0b0000_0001, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + ); + + // Bits are packed LSB-first, so [0, 2, 5] set yields 1 + 4 + 32 = 0b0010_0101. + let active_fields = ActiveFields { + active_fields: vec![true, false, true, false, false, true], + }; + assert_eq!( + active_fields.packed(), + [ + 0b0010_0101, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + ); + } + + #[test] + fn active_fields_packed_multi_byte() { + // Bits 0 and 10 set, spanning two bytes: byte 0 has bit 0, byte 1 has bit 2 (10 % 8). + let mut bits = vec![false; 11]; + bits[0] = true; + bits[10] = true; + let packed = ActiveFields::new(bits).unwrap().packed(); + + assert_eq!(packed[0], 0b0000_0001); + assert_eq!(packed[1], 0b0000_0100); + assert!(packed[2..].iter().all(|&b| b == 0)); + } + + #[test] + fn active_fields_new_validation() { + // Empty is rejected. + assert!(ActiveFields::new(vec![]).is_err()); + // A trailing `0` (inactive last field) is rejected. + assert!(ActiveFields::new(vec![false]).is_err()); + assert!(ActiveFields::new(vec![true, false]).is_err()); + // More than MAX_ACTIVE_FIELDS entries is rejected. + assert!(ActiveFields::new(vec![true; MAX_ACTIVE_FIELDS + 1]).is_err()); + + // Valid cases. + assert!(ActiveFields::new(vec![true]).is_ok()); + assert!(ActiveFields::new(vec![false, true]).is_ok()); + assert!(ActiveFields::new(vec![true; MAX_ACTIVE_FIELDS]).is_ok()); + } +} diff --git a/tree_hash_derive/src/lib.rs b/tree_hash_derive/src/lib.rs index 6e6a727..1e4fddc 100644 --- a/tree_hash_derive/src/lib.rs +++ b/tree_hash_derive/src/lib.rs @@ -1,45 +1,18 @@ #![recursion_limit = "256"] -use darling::FromDeriveInput; +mod attrs; + +use crate::attrs::{EnumBehaviour, StructBehaviour, StructOpts, VariantOpts}; +use darling::{FromDeriveInput, FromMeta}; use proc_macro::TokenStream; use quote::quote; +use std::collections::HashSet; use std::convert::TryInto; -use syn::{parse_macro_input, DataEnum, DataStruct, DeriveInput, Ident}; +use syn::{parse_macro_input, Attribute, DataEnum, DataStruct, DeriveInput, Ident}; /// The highest possible union selector value (higher values are reserved for backwards compatible /// extensions). const MAX_UNION_SELECTOR: u8 = 127; -#[derive(Debug, FromDeriveInput)] -#[darling(attributes(tree_hash))] -struct StructOpts { - #[darling(default)] - enum_behaviour: Option, -} - -const ENUM_TRANSPARENT: &str = "transparent"; -const ENUM_UNION: &str = "union"; -const ENUM_VARIANTS: &[&str] = &[ENUM_TRANSPARENT, ENUM_UNION]; -const NO_ENUM_BEHAVIOUR_ERROR: &str = "enums require an \"enum_behaviour\" attribute, \ - e.g., #[tree_hash(enum_behaviour = \"transparent\")]"; - -enum EnumBehaviour { - Transparent, - Union, -} - -impl EnumBehaviour { - pub fn new(s: Option) -> Option { - s.map(|s| match s.as_ref() { - ENUM_TRANSPARENT => EnumBehaviour::Transparent, - ENUM_UNION => EnumBehaviour::Union, - other => panic!( - "{} is an invalid enum_behaviour, use either {:?}", - other, ENUM_VARIANTS - ), - }) - } -} - /// Return a Vec of `syn::Ident` for each named field in the struct, whilst filtering out fields /// that should not be hashed. /// @@ -78,44 +51,143 @@ fn get_hashable_fields_and_their_caches( /// The field attribute is: `#[tree_hash(skip_hashing)]` fn should_skip_hashing(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path().is_ident("tree_hash") && attr.parse_args::().unwrap() == "skip_hashing" + is_tree_hash_attr(attr) + && attr + .parse_args::() + .is_ok_and(|ident| ident == "skip_hashing") }) } -/// Implements `tree_hash::TreeHash` for some `struct`. +/// Implements `tree_hash::TreeHash` for a type. /// /// Fields are hashed in the order they are defined. #[proc_macro_derive(TreeHash, attributes(tree_hash))] pub fn tree_hash_derive(input: TokenStream) -> TokenStream { let item = parse_macro_input!(input as DeriveInput); let opts = StructOpts::from_derive_input(&item).unwrap(); - let enum_opt = EnumBehaviour::new(opts.enum_behaviour); + let enum_opt = opts.enum_behaviour; + let struct_opt = opts.struct_behaviour; - match &item.data { - syn::Data::Struct(s) => { + match (&item.data, enum_opt, struct_opt) { + (syn::Data::Struct(s), enum_opt, struct_opt) => { if enum_opt.is_some() { panic!("enum_behaviour is invalid for structs"); } - tree_hash_derive_struct(&item, s) + let struct_behaviour = struct_opt.unwrap_or_default(); + tree_hash_derive_struct(&item, s, struct_behaviour, opts.active_fields) + } + (syn::Data::Enum(s), Some(enum_behaviour), struct_opt) => { + if struct_opt.is_some() { + panic!("struct_behaviour is invalid for enums"); + } + if opts.active_fields.is_some() { + panic!("active_fields is invalid for enums"); + } + match enum_behaviour { + EnumBehaviour::Transparent => tree_hash_derive_enum_transparent(&item, s), + EnumBehaviour::Union | EnumBehaviour::CompatibleUnion => { + tree_hash_derive_enum_union(&item, s, enum_behaviour) + } + } } - syn::Data::Enum(s) => match enum_opt.expect(NO_ENUM_BEHAVIOUR_ERROR) { - EnumBehaviour::Transparent => tree_hash_derive_enum_transparent(&item, s), - EnumBehaviour::Union => tree_hash_derive_enum_union(&item, s), - }, - _ => panic!("tree_hash_derive only supports structs and enums."), + _ => panic!("tree_hash_derive only supports structs and enums"), } } -fn tree_hash_derive_struct(item: &DeriveInput, struct_data: &DataStruct) -> TokenStream { +fn tree_hash_derive_struct( + item: &DeriveInput, + struct_data: &DataStruct, + struct_behaviour: StructBehaviour, + active_fields_opt: Option, +) -> TokenStream { let name = &item.ident; let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); let idents = get_hashable_fields(struct_data); - let num_leaves = idents.len(); + + let hasher_init = if let StructBehaviour::ProgressiveContainer = struct_behaviour { + quote! { tree_hash::ProgressiveMerkleHasher::new() } + } else { + let num_leaves = idents.len(); + quote! { tree_hash::MerkleHasher::with_leaves(#num_leaves) } + }; + + // Compute the field hashes while accounting for inactive fields which hash as 0x0. + // + // The `mixin_logic` is the expression to mix in the `active_fields` in the case of a + // progressive container. + let (field_hashes, mixin_logic) = if let StructBehaviour::ProgressiveContainer = + struct_behaviour + { + let Some(active_fields) = active_fields_opt else { + panic!("active_fields must be provided for progressive_container"); + }; + + // Map each `active_fields` entry to a leaf: an active entry consumes the next hashable + // struct field (fields marked `#[tree_hash(skip_hashing)]` are already excluded from + // `idents`), while an inactive entry contributes a zero leaf so that chunk positions + // (and therefore gindices) stay stable. + let mut active_field_index = 0; + let mut field_hashes: Vec = vec![]; + for active in &active_fields.active_fields { + if *active { + let Some(ident) = idents.get(active_field_index) else { + panic!( + "active_fields is inconsistent with struct fields. \ + index: {active_field_index}, hashable fields: {}", + idents.len() + ) + }; + active_field_index += 1; + field_hashes.push(quote! { self.#ident.tree_hash_root() }); + } else { + field_hashes.push(quote! { tree_hash::Hash256::ZERO }); + } + } + + // Every hashable field must be claimed by an active entry. Without this check an + // under-specified `active_fields` would silently drop trailing fields from the hash. + // (The over-specified case is already caught above by `idents.get(..)` returning + // `None`.) + assert_eq!( + active_field_index, + idents.len(), + "active_fields has {active_field_index} active entries but the struct has {} \ + hashable fields (fields marked #[tree_hash(skip_hashing)] are not counted)", + idents.len() + ); + + let packed_active_fields = active_fields.packed_tokens(); + let active_fields_len = attrs::ACTIVE_FIELDS_PACKED_BYTES_LEN; + + let mixin_logic = quote! { + const ACTIVE_FIELDS: [u8; #active_fields_len] = #packed_active_fields; + // The packed active_fields must occupy exactly one chunk for `mix_in_active_fields`. + const _: () = assert!(#active_fields_len == tree_hash::BYTES_PER_CHUNK); + tree_hash::mix_in_active_fields(&container_root, ACTIVE_FIELDS) + }; + + (field_hashes, mixin_logic) + } else { + if active_fields_opt.is_some() { + panic!("active_fields is only valid with struct_behaviour = \"progressive_container\""); + } + ( + idents + .into_iter() + .map(|ident| quote! { self.#ident.tree_hash_root() }) + .collect(), + quote! { container_root }, + ) + }; let output = quote! { impl #impl_generics tree_hash::TreeHash for #name #ty_generics #where_clause { fn tree_hash_type() -> tree_hash::TreeHashType { + // There is no specific type for progressive containers, although we could consider + // adding one in future if it would prove useful. + // + // See: https://github.com/sigp/tree_hash/issues/44 tree_hash::TreeHashType::Container } @@ -128,14 +200,16 @@ fn tree_hash_derive_struct(item: &DeriveInput, struct_data: &DataStruct) -> Toke } fn tree_hash_root(&self) -> tree_hash::Hash256 { - let mut hasher = tree_hash::MerkleHasher::with_leaves(#num_leaves); + let mut hasher = #hasher_init; #( - hasher.write(self.#idents.tree_hash_root().as_slice()) + hasher.write(#field_hashes.as_slice()) .expect("tree hash derive should not apply too many leaves"); )* - hasher.finish().expect("tree hash derive should not have a remaining buffer") + let container_root = hasher.finish().expect("tree hash derive should not have a remaining buffer"); + + #mixin_logic } } }; @@ -220,19 +294,30 @@ fn tree_hash_derive_enum_transparent( output.into() } -/// Derive `TreeHash` for an `enum` following the "union" SSZ spec. +/// Derive `TreeHash` for an `enum` following the compatible union or ordinary union SSZ spec. /// -/// The union selector will be determined based upon the order in which the enum variants are -/// defined. E.g., the top-most variant in the enum will have a selector of `0`, the variant -/// beneath it will have a selector of `1` and so on. +/// The union selectors for a compatible union MUST be defined using the variant attribute: +/// +/// - `tree_hash(selector = "X")` +/// +/// The union selectors for an ordinary union will be determined based upon the order in which the +/// enum variants are defined. E.g., the top-most variant in the enum will have a selector of `0`, +/// the variant beneath it will have a selector of `1` and so on. /// /// # Limitations /// /// Only supports enums where each variant has a single field. -fn tree_hash_derive_enum_union(derive_input: &DeriveInput, enum_data: &DataEnum) -> TokenStream { +fn tree_hash_derive_enum_union( + derive_input: &DeriveInput, + enum_data: &DataEnum, + enum_behaviour: EnumBehaviour, +) -> TokenStream { let name = &derive_input.ident; let (impl_generics, ty_generics, where_clause) = &derive_input.generics.split_for_impl(); + // Parse variant-level configuration. + let variant_opts = parse_variant_opts(enum_data); + let patterns: Vec<_> = enum_data .variants .iter() @@ -249,7 +334,18 @@ fn tree_hash_derive_enum_union(derive_input: &DeriveInput, enum_data: &DataEnum) }) .collect(); - let union_selectors = compute_union_selectors(patterns.len()); + let union_selectors = match enum_behaviour { + EnumBehaviour::CompatibleUnion => get_compatible_union_selectors(enum_data, &variant_opts), + EnumBehaviour::Union => { + // For now we don't allow selectors in ordinary unions to be set manually. + assert!( + variant_opts.iter().all(|opt| opt.selector.is_none()), + "specifying the selector in a regular union is not supported" + ); + compute_union_selectors(patterns.len()) + } + EnumBehaviour::Transparent => unreachable!("union code called for transparent enum"), + }; let output = quote! { impl #impl_generics tree_hash::TreeHash for #name #ty_generics #where_clause { @@ -282,6 +378,74 @@ fn tree_hash_derive_enum_union(derive_input: &DeriveInput, enum_data: &DataEnum) output.into() } +fn parse_variant_opts(enum_data: &DataEnum) -> Vec { + enum_data + .variants + .iter() + .map(|variant| { + let tree_hash_attrs = variant + .attrs + .iter() + .filter(|attr| is_tree_hash_attr(attr)) + .collect::>(); + let ssz_attrs = variant + .attrs + .iter() + .filter(|attr| is_ssz_attr(attr)) + .collect::>(); + + // Check for duplicate `tree_hash` attributes. + // Checking duplicate `ssz` attributes is the job of the `ssz_derive` macro. + if tree_hash_attrs.len() > 1 { + panic!("more than one variant-level \"tree_hash\" attribute provided"); + } + + let variant_name = &variant.ident; + let parse_opts = |meta: &syn::Meta| { + VariantOpts::from_meta(meta).unwrap_or_else(|e| { + panic!( + "failed to parse variant attribute for \"{variant_name}\": {e}; \ + note that `selector` must be an integer in 1..={MAX_UNION_SELECTOR}" + ) + }) + }; + + let tree_hash_opts = tree_hash_attrs.first().map(|attr| parse_opts(&attr.meta)); + let ssz_opts = ssz_attrs.first().map(|attr| parse_opts(&attr.meta)); + + // Check consistency with SSZ opts, or fall back to SSZ attribute if tree_hash attribute + // is absent. + match (tree_hash_opts, ssz_opts) { + (Some(tree_hash), Some(ssz)) => { + assert_eq!( + tree_hash, ssz, + "inconsistent \"tree_hash\" and \"ssz\" attributes" + ); + tree_hash + } + (Some(attr), None) | (None, Some(attr)) => attr, + (None, None) => VariantOpts::default(), + } + }) + .collect() +} + +/// Predicate for determining whether an attribute is a `tree_hash` attribute. +fn is_tree_hash_attr(attr: &Attribute) -> bool { + is_attr_with_ident(attr, "tree_hash") +} + +fn is_ssz_attr(attr: &Attribute) -> bool { + is_attr_with_ident(attr, "ssz") +} + +/// Predicate for determining whether an attribute has the given `ident` as its path. +fn is_attr_with_ident(attr: &Attribute, ident: &str) -> bool { + attr.path() + .get_ident() + .is_some_and(|attr_ident| *attr_ident == ident) +} + fn compute_union_selectors(num_variants: usize) -> Vec { let union_selectors = (0..num_variants) .map(|i| { @@ -304,3 +468,30 @@ fn compute_union_selectors(num_variants: usize) -> Vec { union_selectors } + +fn get_compatible_union_selectors(enum_data: &DataEnum, variant_opts: &[VariantOpts]) -> Vec { + let mut seen_selectors = HashSet::new(); + enum_data + .variants + .iter() + .zip(variant_opts.iter()) + .map(|(variant, variant_opt)| { + let variant_name = &variant.ident; + let Some(selector) = variant_opt.selector else { + panic!("you must define a selector for variant \"{variant_name}\""); + }; + if selector == 0 || selector > MAX_UNION_SELECTOR { + panic!( + "selector = {selector} for variant \"{variant_name}\" is illegal in a \ + compatible union" + ); + } + if !seen_selectors.insert(selector) { + panic!( + "selector = {selector} for variant \"{variant_name}\" is used more than once" + ); + } + selector + }) + .collect() +}