From d7e02dbb4e2829cc094a1365f4d8f118d7f470c0 Mon Sep 17 00:00:00 2001 From: Mac L Date: Wed, 10 Jun 2026 22:40:42 +1000 Subject: [PATCH 1/4] Add ProgressiveVariableList --- Cargo.toml | 5 + src/context_deserialize.rs | 16 +- src/lib.rs | 2 + src/progressive_variable_list.rs | 370 ++++++++++++++++++ src/serde_utils/hex_prog_var_list.rs | 53 +++ src/serde_utils/list_of_hex_prog_fixed_vec.rs | 73 ++++ src/serde_utils/list_of_hex_prog_var_list.rs | 97 +++++ src/serde_utils/mod.rs | 3 + src/tree_hash.rs | 31 +- 9 files changed, 648 insertions(+), 2 deletions(-) create mode 100644 src/progressive_variable_list.rs create mode 100644 src/serde_utils/hex_prog_var_list.rs create mode 100644 src/serde_utils/list_of_hex_prog_fixed_vec.rs create mode 100644 src/serde_utils/list_of_hex_prog_var_list.rs diff --git a/Cargo.toml b/Cargo.toml index a157a89..ba63e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,8 @@ tree_hash_derive = "0.12.0" [[bench]] harness = false name = "encode_decode" + +[patch.crates-io] +tree_hash = { git = "https://github.com/sigp/tree_hash", branch = "progressive" } +tree_hash_derive = { git = "https://github.com/sigp/tree_hash", branch = "progressive" } +ethereum_ssz = { git = "https://github.com/sigp/ethereum_ssz", branch = "progressive" } diff --git a/src/context_deserialize.rs b/src/context_deserialize.rs index 50fd9b7..870ff9f 100644 --- a/src/context_deserialize.rs +++ b/src/context_deserialize.rs @@ -1,4 +1,4 @@ -use crate::{typenum::Unsigned, FixedVector}; +use crate::{typenum::Unsigned, FixedVector, ProgressiveVariableList}; use context_deserialize::ContextDeserialize; use serde::de::{Deserializer, Error}; @@ -16,3 +16,17 @@ where FixedVector::new(vec).map_err(|e| D::Error::custom(format!("{:?}", e))) } } + +impl<'de, C, T> ContextDeserialize<'de, C> for ProgressiveVariableList +where + T: ContextDeserialize<'de, C>, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>, + { + let vec = Vec::::context_deserialize(deserializer, context)?; + Ok(ProgressiveVariableList::new(vec)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 967562d..7fbbdc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub mod length { #[macro_use] mod fixed_vector; +mod progressive_variable_list; mod tree_hash; mod variable_list; @@ -51,6 +52,7 @@ mod variable_list; mod context_deserialize; pub use fixed_vector::FixedVector; +pub use progressive_variable_list::ProgressiveVariableList; pub use ssz::{BitList, BitVector, Bitfield}; pub use typenum; pub use variable_list::VariableList; diff --git a/src/progressive_variable_list.rs b/src/progressive_variable_list.rs new file mode 100644 index 0000000..8ae0be1 --- /dev/null +++ b/src/progressive_variable_list.rs @@ -0,0 +1,370 @@ +use crate::tree_hash::progressive_vec_tree_hash_root; +use serde::Deserialize; +use serde_derive::Serialize; +use std::any::TypeId; +use std::mem; +use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::slice::SliceIndex; +use tree_hash::Hash256; + +/// Emulates a SSZ `ProgressiveList` (EIP-7916). +/// +/// An ordered, heap-allocated, variable-length, homogeneous collection of `T` with **no** capacity +/// limit. This is the progressive analogue of [`VariableList`](crate::VariableList): the two are +/// identical except that +/// +/// - there is no type-level maximum length (`N`), so construction and `push` are infallible, and +/// - merkleization uses the progressive scheme of EIP-7916 (a right-leaning spine of binary +/// subtrees whose capacities grow by 4x), so the hash tree root is independent of any limit. +/// +/// Like `VariableList`, it is backed by a Rust `Vec` and serialized identically to a plain list. +/// +/// ## Example +/// +/// ``` +/// use ssz_types::ProgressiveVariableList; +/// +/// let base: Vec = vec![1, 2, 3, 4]; +/// +/// let mut list: ProgressiveVariableList = ProgressiveVariableList::new(base.clone()); +/// assert_eq!(&list[..], &[1, 2, 3, 4]); +/// +/// // Unlike `VariableList`, `push` cannot fail. +/// list.push(5); +/// assert_eq!(&list[..], &[1, 2, 3, 4, 5]); +/// ``` +#[derive(Clone, Serialize)] +#[serde(transparent)] +pub struct ProgressiveVariableList { + vec: Vec, +} + +impl PartialEq for ProgressiveVariableList { + fn eq(&self, other: &Self) -> bool { + self.vec == other.vec + } +} +impl Eq for ProgressiveVariableList {} +impl std::hash::Hash for ProgressiveVariableList { + fn hash(&self, state: &mut H) { + self.vec.hash(state); + } +} + +impl std::fmt::Debug for ProgressiveVariableList { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.vec.fmt(f) + } +} + +impl ProgressiveVariableList { + /// Create a list from a `Vec`. Infallible, as there is no capacity limit. + pub fn new(vec: Vec) -> Self { + Self { vec } + } + + /// Create an empty list. + pub fn empty() -> Self { + Self { vec: vec![] } + } + + /// Returns the number of values presently in `self`. + pub fn len(&self) -> usize { + self.vec.len() + } + + /// True if `self` does not contain any values. + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + /// Appends `value` to the back of `self`. Infallible, as there is no capacity limit. + pub fn push(&mut self, value: T) { + self.vec.push(value); + } + + /// Returns the contents as a slice. + pub fn as_slice(&self) -> &[T] { + &self.vec + } + + /// Consumes `self`, returning the underlying `Vec`. + pub fn into_vec(self) -> Vec { + self.vec + } +} + +impl From> for ProgressiveVariableList { + fn from(vec: Vec) -> Self { + Self::new(vec) + } +} + +impl From> for Vec { + fn from(list: ProgressiveVariableList) -> Vec { + list.vec + } +} + +impl Default for ProgressiveVariableList { + fn default() -> Self { + Self { + vec: Vec::default(), + } + } +} + +impl FromIterator for ProgressiveVariableList { + fn from_iter>(iter: I) -> Self { + Self::new(iter.into_iter().collect()) + } +} + +impl> Index for ProgressiveVariableList { + type Output = I::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + Index::index(&self.vec, index) + } +} + +impl> IndexMut for ProgressiveVariableList { + #[inline] + fn index_mut(&mut self, index: I) -> &mut Self::Output { + IndexMut::index_mut(&mut self.vec, index) + } +} + +impl Deref for ProgressiveVariableList { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + +impl DerefMut for ProgressiveVariableList { + fn deref_mut(&mut self) -> &mut [T] { + &mut self.vec[..] + } +} + +impl AsRef<[T]> for ProgressiveVariableList { + fn as_ref(&self) -> &[T] { + &self.vec[..] + } +} + +impl<'a, T> IntoIterator for &'a ProgressiveVariableList { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for ProgressiveVariableList { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl tree_hash::TreeHash for ProgressiveVariableList +where + T: tree_hash::TreeHash, +{ + fn tree_hash_type() -> tree_hash::TreeHashType { + tree_hash::TreeHashType::List + } + + fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding { + unreachable!("List should never be packed.") + } + + fn tree_hash_packing_factor() -> usize { + unreachable!("List should never be packed.") + } + + fn tree_hash_root(&self) -> Hash256 { + let root = progressive_vec_tree_hash_root::(&self.vec); + + tree_hash::mix_in_length(&root, self.len()) + } +} + +impl ssz::Encode for ProgressiveVariableList +where + T: ssz::Encode, +{ + fn is_ssz_fixed_len() -> bool { + >::is_ssz_fixed_len() + } + + fn ssz_fixed_len() -> usize { + >::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.vec.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.vec.ssz_append(buf) + } +} + +impl ssz::TryFromIter for ProgressiveVariableList { + type Error = std::convert::Infallible; + + fn try_from_iter(value: I) -> Result + where + I: IntoIterator, + { + Ok(Self::new(value.into_iter().collect())) + } +} + +impl ssz::Decode for ProgressiveVariableList +where + T: ssz::Decode + 'static, +{ + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(Self::default()); + } + + if TypeId::of::() == TypeId::of::() { + // Safety: We've verified T is u8, so Vec *is* Vec. + let vec_u8 = bytes.to_vec(); + let vec_t = unsafe { mem::transmute::, Vec>(vec_u8) }; + return Ok(Self::new(vec_t)); + } + + if T::is_ssz_fixed_len() { + let item_len = T::ssz_fixed_len(); + if !bytes.len().is_multiple_of(item_len) { + return Err(ssz::DecodeError::BytesInvalid(format!( + "ProgressiveVariableList has {} bytes, not a multiple of item length {}", + bytes.len(), + item_len + ))); + } + + bytes + .chunks_exact(item_len) + .map(T::from_ssz_bytes) + .collect::, _>>() + .map(Self::new) + } else { + ssz::decode_list_of_variable_length_items(bytes, None).map(Self::new) + } + } +} + +impl<'de, T> Deserialize<'de> for ProgressiveVariableList +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self::new(Vec::::deserialize(deserializer)?)) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a, T: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a> for ProgressiveVariableList { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self::new(>::arbitrary(u)?)) + } + + fn size_hint(depth: usize) -> (usize, Option) { + >::size_hint(depth) + } +} + +#[cfg(test)] +mod test { + use super::*; + use ssz::{Decode, Encode}; + use tree_hash::TreeHash; + + #[test] + fn new_and_push_infallible() { + let mut list: ProgressiveVariableList = ProgressiveVariableList::new(vec![1, 2, 3]); + assert_eq!(&list[..], &[1, 2, 3]); + list.push(4); + assert_eq!(&list[..], &[1, 2, 3, 4]); + assert_eq!(list.len(), 4); + } + + fn ssz_round_trip(item: T) { + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!(T::from_ssz_bytes(encoded), Ok(item)); + } + + #[test] + fn ssz_round_trip_bytes() { + ssz_round_trip::>(ProgressiveVariableList::new(vec![])); + ssz_round_trip::>(ProgressiveVariableList::new(vec![42; 100])); + // Serializes identically to a plain byte-list. + let bytes = ProgressiveVariableList::::new(vec![1, 2, 3]); + assert_eq!(bytes.as_ssz_bytes(), vec![1, 2, 3]); + } + + #[test] + fn ssz_round_trip_u64() { + ssz_round_trip::>(ProgressiveVariableList::new(vec![42; 9])); + } + + #[test] + fn serde_is_a_sequence() { + // Matches `VariableList`: the default serde representation is a JSON sequence, not hex. + let list: ProgressiveVariableList = ProgressiveVariableList::new(vec![1, 2, 255]); + let json = serde_json::to_string(&list).unwrap(); + assert_eq!(json, "[1,2,255]"); + assert_eq!( + serde_json::from_str::>(&json).unwrap(), + list + ); + } + + #[test] + fn tree_hash_byte_list() { + use crate::VariableList; + use tree_hash::mix_in_length; + use typenum::U256; + + // Empty list: mix_in_length(ZERO, 0). (Exact progressive-hasher math is covered by the + // `tree_hash` crate's own tests and the EF spec vectors.) + assert_eq!( + ProgressiveVariableList::::empty().tree_hash_root(), + mix_in_length(&Hash256::ZERO, 0) + ); + + // Deterministic and non-zero for a non-empty list. + let bytes: Vec = (0..40).collect(); + let root = ProgressiveVariableList::new(bytes.clone()).tree_hash_root(); + assert_eq!( + root, + ProgressiveVariableList::new(bytes.clone()).tree_hash_root() + ); + assert_ne!(root, mix_in_length(&Hash256::ZERO, bytes.len())); + + // Progressive merkleization differs from the fixed-depth `VariableList` root: same data, + // different scheme. + let bounded = VariableList::::new(bytes).unwrap(); + assert_ne!(root, bounded.tree_hash_root()); + } +} diff --git a/src/serde_utils/hex_prog_var_list.rs b/src/serde_utils/hex_prog_var_list.rs new file mode 100644 index 0000000..38c728e --- /dev/null +++ b/src/serde_utils/hex_prog_var_list.rs @@ -0,0 +1,53 @@ +//! Serialize `ProgressiveVariableList` as a 0x-prefixed hex string. +//! +//! The progressive (EIP-7688) counterpart of [`hex_var_list`](super::hex_var_list). +use crate::ProgressiveVariableList; +use serde::{Deserializer, Serializer}; +use serde_utils::hex::{self, PrefixedHexVisitor}; + +pub fn serialize(bytes: &ProgressiveVariableList, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&hex::encode(&**bytes)) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; + Ok(ProgressiveVariableList::new(bytes)) +} + +#[cfg(test)] +mod test { + use crate::ProgressiveVariableList; + use serde_derive::{Deserialize, Serialize}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Obj { + #[serde(with = "crate::serde_utils::hex_prog_var_list")] + bytes: ProgressiveVariableList, + } + + #[test] + fn round_trip_hex() { + let obj = Obj { + bytes: ProgressiveVariableList::new(vec![1, 2, 3, 255]), + }; + let json = serde_json::to_string(&obj).unwrap(); + assert_eq!(json, r#"{"bytes":"0x010203ff"}"#); + assert_eq!(serde_json::from_str::(&json).unwrap(), obj); + } + + #[test] + fn empty() { + let obj = Obj { + bytes: ProgressiveVariableList::empty(), + }; + let json = serde_json::to_string(&obj).unwrap(); + assert_eq!(json, r#"{"bytes":"0x"}"#); + assert_eq!(serde_json::from_str::(&json).unwrap(), obj); + } +} diff --git a/src/serde_utils/list_of_hex_prog_fixed_vec.rs b/src/serde_utils/list_of_hex_prog_fixed_vec.rs new file mode 100644 index 0000000..d127eb2 --- /dev/null +++ b/src/serde_utils/list_of_hex_prog_fixed_vec.rs @@ -0,0 +1,73 @@ +//! Serialize `ProgressiveVariableList>` as list of 0x-prefixed hex string. +//! +//! The progressive (EIP-7688) counterpart of [`list_of_hex_fixed_vec`](super::list_of_hex_fixed_vec). +use crate::{FixedVector, ProgressiveVariableList}; +use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; +use std::marker::PhantomData; +use typenum::Unsigned; + +#[derive(Deserialize)] +#[serde(transparent)] +pub struct WrappedListOwned( + #[serde(with = "crate::serde_utils::hex_fixed_vec")] FixedVector, +); + +#[derive(Serialize)] +#[serde(transparent)] +pub struct WrappedListRef<'a, N: Unsigned>( + #[serde(with = "crate::serde_utils::hex_fixed_vec")] &'a FixedVector, +); + +pub fn serialize( + list: &ProgressiveVariableList>, + serializer: S, +) -> Result +where + S: Serializer, + M: Unsigned, +{ + let mut seq = serializer.serialize_seq(Some(list.len()))?; + for bytes in list { + seq.serialize_element(&WrappedListRef(bytes))?; + } + seq.end() +} + +#[derive(Default)] +pub struct Visitor { + _phantom_m: PhantomData, +} + +impl<'a, M> serde::de::Visitor<'a> for Visitor +where + M: Unsigned, +{ + type Value = ProgressiveVariableList>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a list of 0x-prefixed hex bytes") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'a>, + { + let mut list = ProgressiveVariableList::empty(); + + while let Some(val) = seq.next_element::>()? { + list.push(val.0); + } + + Ok(list) + } +} + +pub fn deserialize<'de, D, M>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, + M: Unsigned, +{ + deserializer.deserialize_seq(Visitor::default()) +} diff --git a/src/serde_utils/list_of_hex_prog_var_list.rs b/src/serde_utils/list_of_hex_prog_var_list.rs new file mode 100644 index 0000000..8718c05 --- /dev/null +++ b/src/serde_utils/list_of_hex_prog_var_list.rs @@ -0,0 +1,97 @@ +//! Serialize `ProgressiveVariableList>` as a list of 0x-prefixed hex +//! strings. +//! +//! The progressive (EIP-7688) counterpart of [`list_of_hex_var_list`](super::list_of_hex_var_list). +use crate::ProgressiveVariableList; +use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Deserialize)] +#[serde(transparent)] +pub struct WrappedListOwned( + #[serde(with = "crate::serde_utils::hex_prog_var_list")] ProgressiveVariableList, +); + +#[derive(Serialize)] +#[serde(transparent)] +pub struct WrappedListRef<'a>( + #[serde(with = "crate::serde_utils::hex_prog_var_list")] &'a ProgressiveVariableList, +); + +pub fn serialize( + list: &ProgressiveVariableList>, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(list.len()))?; + for bytes in list { + seq.serialize_element(&WrappedListRef(bytes))?; + } + seq.end() +} + +struct Visitor; + +impl<'a> serde::de::Visitor<'a> for Visitor { + type Value = ProgressiveVariableList>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a list of 0x-prefixed hex strings") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'a>, + { + let mut list = Vec::new(); + while let Some(val) = seq.next_element::()? { + list.push(val.0); + } + Ok(ProgressiveVariableList::new(list)) + } +} + +pub fn deserialize<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(Visitor) +} + +#[cfg(test)] +mod test { + use crate::ProgressiveVariableList; + use serde_derive::{Deserialize, Serialize}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Obj { + #[serde(with = "crate::serde_utils::list_of_hex_prog_var_list")] + lists: ProgressiveVariableList>, + } + + #[test] + fn round_trip_hex() { + let obj = Obj { + lists: ProgressiveVariableList::new(vec![ + ProgressiveVariableList::new(vec![1, 2, 3]), + ProgressiveVariableList::new(vec![255]), + ]), + }; + let json = serde_json::to_string(&obj).unwrap(); + assert_eq!(json, r#"{"lists":["0x010203","0xff"]}"#); + assert_eq!(serde_json::from_str::(&json).unwrap(), obj); + } + + #[test] + fn empty() { + let obj = Obj { + lists: ProgressiveVariableList::empty(), + }; + let json = serde_json::to_string(&obj).unwrap(); + assert_eq!(json, r#"{"lists":[]}"#); + assert_eq!(serde_json::from_str::(&json).unwrap(), obj); + } +} diff --git a/src/serde_utils/mod.rs b/src/serde_utils/mod.rs index 4417f5a..c408084 100644 --- a/src/serde_utils/mod.rs +++ b/src/serde_utils/mod.rs @@ -1,6 +1,9 @@ pub mod hex_fixed_vec; +pub mod hex_prog_var_list; pub mod hex_var_list; pub mod list_of_hex_fixed_vec; +pub mod list_of_hex_prog_fixed_vec; +pub mod list_of_hex_prog_var_list; pub mod list_of_hex_var_list; pub mod quoted_u64_fixed_vec; pub mod quoted_u64_var_list; diff --git a/src/tree_hash.rs b/src/tree_hash.rs index 4380bcd..c2a0ae6 100644 --- a/src/tree_hash.rs +++ b/src/tree_hash.rs @@ -1,4 +1,4 @@ -use tree_hash::{Hash256, MerkleHasher, TreeHash, TreeHashType}; +use tree_hash::{Hash256, MerkleHasher, ProgressiveMerkleHasher, TreeHash, TreeHashType}; /// A helper function providing common functionality between the `TreeHash` implementations for /// `FixedVector` and `VariableList`. @@ -36,3 +36,32 @@ where } } } + +/// Like `vec_tree_hash_root`, but uses progressive merkleization from EIP-7916. +pub fn progressive_vec_tree_hash_root(vec: &[T]) -> Hash256 +where + T: TreeHash, +{ + let mut hasher = ProgressiveMerkleHasher::new(); + + match T::tree_hash_type() { + TreeHashType::Basic => { + for item in vec { + hasher + .write(&item.tree_hash_packed_encoding()) + .expect("ssz_types progressive vec should not exceed the leaf limit"); + } + } + TreeHashType::Container | TreeHashType::List | TreeHashType::Vector => { + for item in vec { + hasher + .write(item.tree_hash_root().as_slice()) + .expect("ssz_types progressive vec should not exceed the leaf limit"); + } + } + } + + hasher + .finish() + .expect("ssz_types progressive vec should not have a remaining buffer") +} From 46e6a700bbc581b7bdc5d8e4a2b73a1288e75c55 Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 30 Jun 2026 03:08:58 +1000 Subject: [PATCH 2/4] General tidy up --- src/lib.rs | 14 +++ src/progressive_variable_list.rs | 13 +-- src/serde_utils/list_of_hex_fixed_vec.rs | 4 +- src/serde_utils/list_of_hex_prog_fixed_vec.rs | 15 +--- src/tree_hash.rs | 90 +++++++++++-------- src/variable_list.rs | 6 +- 6 files changed, 82 insertions(+), 60 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7fbbdc3..83703c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,20 @@ mod list_encoded_option; pub use list_encoded_option::ListEncodedOption; +/// Reinterpret `bytes` as a `Vec`, for the common `T == u8` SSZ fast-path. +/// +/// Safety: the caller must have verified `TypeId::of::() == TypeId::of::()`, so that +/// `Vec` and `Vec` have identical layout. +pub(crate) fn u8_bytes_to_vec(bytes: &[u8]) -> Vec { + debug_assert_eq!( + core::any::TypeId::of::(), + core::any::TypeId::of::(), + "u8_bytes_to_vec called with T != u8", + ); + // Safety: caller verified `T == u8`, so the transmute is a no-op reinterpretation. + unsafe { core::mem::transmute::, Vec>(bytes.to_vec()) } +} + /// Returned when an item encounters an error. #[derive(PartialEq, Debug, Clone)] pub enum Error { diff --git a/src/progressive_variable_list.rs b/src/progressive_variable_list.rs index 8ae0be1..ab4e22e 100644 --- a/src/progressive_variable_list.rs +++ b/src/progressive_variable_list.rs @@ -2,7 +2,6 @@ use crate::tree_hash::progressive_vec_tree_hash_root; use serde::Deserialize; use serde_derive::Serialize; use std::any::TypeId; -use std::mem; use std::ops::{Deref, DerefMut, Index, IndexMut}; use std::slice::SliceIndex; use tree_hash::Hash256; @@ -243,14 +242,18 @@ where } if TypeId::of::() == TypeId::of::() { - // Safety: We've verified T is u8, so Vec *is* Vec. - let vec_u8 = bytes.to_vec(); - let vec_t = unsafe { mem::transmute::, Vec>(vec_u8) }; - return Ok(Self::new(vec_t)); + return Ok(Self::new(crate::u8_bytes_to_vec(bytes))); } if T::is_ssz_fixed_len() { let item_len = T::ssz_fixed_len(); + // A zero-length item is a distinct error, matching `VariableList::from_ssz_bytes`. + // It also guards the `chunks_exact` below against a zero divisor. + bytes + .len() + .checked_div(item_len) + .ok_or(ssz::DecodeError::ZeroLengthItem)?; + if !bytes.len().is_multiple_of(item_len) { return Err(ssz::DecodeError::BytesInvalid(format!( "ProgressiveVariableList has {} bytes, not a multiple of item length {}", diff --git a/src/serde_utils/list_of_hex_fixed_vec.rs b/src/serde_utils/list_of_hex_fixed_vec.rs index b93c869..444803d 100644 --- a/src/serde_utils/list_of_hex_fixed_vec.rs +++ b/src/serde_utils/list_of_hex_fixed_vec.rs @@ -7,13 +7,13 @@ use typenum::Unsigned; #[derive(Deserialize)] #[serde(transparent)] pub struct WrappedListOwned( - #[serde(with = "crate::serde_utils::hex_fixed_vec")] FixedVector, + #[serde(with = "crate::serde_utils::hex_fixed_vec")] pub FixedVector, ); #[derive(Serialize)] #[serde(transparent)] pub struct WrappedListRef<'a, N: Unsigned>( - #[serde(with = "crate::serde_utils::hex_fixed_vec")] &'a FixedVector, + #[serde(with = "crate::serde_utils::hex_fixed_vec")] pub &'a FixedVector, ); pub fn serialize( diff --git a/src/serde_utils/list_of_hex_prog_fixed_vec.rs b/src/serde_utils/list_of_hex_prog_fixed_vec.rs index d127eb2..ab32a6a 100644 --- a/src/serde_utils/list_of_hex_prog_fixed_vec.rs +++ b/src/serde_utils/list_of_hex_prog_fixed_vec.rs @@ -2,21 +2,12 @@ //! //! The progressive (EIP-7688) counterpart of [`list_of_hex_fixed_vec`](super::list_of_hex_fixed_vec). use crate::{FixedVector, ProgressiveVariableList}; -use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ser::SerializeSeq, Deserializer, Serializer}; use std::marker::PhantomData; use typenum::Unsigned; -#[derive(Deserialize)] -#[serde(transparent)] -pub struct WrappedListOwned( - #[serde(with = "crate::serde_utils::hex_fixed_vec")] FixedVector, -); - -#[derive(Serialize)] -#[serde(transparent)] -pub struct WrappedListRef<'a, N: Unsigned>( - #[serde(with = "crate::serde_utils::hex_fixed_vec")] &'a FixedVector, -); +// The element wrappers are identical to the bounded `list_of_hex_fixed_vec`, so reuse them. +pub use super::list_of_hex_fixed_vec::{WrappedListOwned, WrappedListRef}; pub fn serialize( list: &ProgressiveVariableList>, diff --git a/src/tree_hash.rs b/src/tree_hash.rs index c2a0ae6..b3a6fe2 100644 --- a/src/tree_hash.rs +++ b/src/tree_hash.rs @@ -1,67 +1,83 @@ use tree_hash::{Hash256, MerkleHasher, ProgressiveMerkleHasher, TreeHash, TreeHashType}; -/// A helper function providing common functionality between the `TreeHash` implementations for -/// `FixedVector` and `VariableList`. -pub fn vec_tree_hash_root(vec: &[T], max_leaves: usize) -> Hash256 -where - T: TreeHash, -{ - match T::tree_hash_type() { - TreeHashType::Basic => { - let mut hasher = - MerkleHasher::with_leaves(max_leaves.div_ceil(T::tree_hash_packing_factor())); +/// A hasher that accepts leaf chunks one at a time and produces a root. +/// +/// Lets `hash_vec` drive either the fixed-depth `MerkleHasher` or the progressive +/// `ProgressiveMerkleHasher` with one shared loop. +trait VecHasher: Sized { + type Error: std::fmt::Debug; - for item in vec { - hasher - .write(&item.tree_hash_packed_encoding()) - .expect("ssz_types variable vec should not contain more elements than max"); - } + fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error>; + fn finish(self) -> Result; +} - hasher - .finish() - .expect("ssz_types variable vec should not have a remaining buffer") - } - TreeHashType::Container | TreeHashType::List | TreeHashType::Vector => { - let mut hasher = MerkleHasher::with_leaves(max_leaves); +impl VecHasher for MerkleHasher { + type Error = tree_hash::Error; - for item in vec { - hasher - .write(item.tree_hash_root().as_slice()) - .expect("ssz_types vec should not contain more elements than max"); - } + fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error> { + MerkleHasher::write(self, bytes) + } + fn finish(self) -> Result { + MerkleHasher::finish(self) + } +} - hasher - .finish() - .expect("ssz_types vec should not have a remaining buffer") - } +impl VecHasher for ProgressiveMerkleHasher { + type Error = tree_hash::ProgressiveMerkleHasherError; + + fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error> { + ProgressiveMerkleHasher::write(self, bytes) + } + fn finish(self) -> Result { + ProgressiveMerkleHasher::finish(self) } } -/// Like `vec_tree_hash_root`, but uses progressive merkleization from EIP-7916. -pub fn progressive_vec_tree_hash_root(vec: &[T]) -> Hash256 +/// Feed `vec` into `hasher` (packed encodings for basic types, chunk roots otherwise) and finish. +fn hash_vec(mut hasher: H, vec: &[T]) -> Hash256 where T: TreeHash, + H: VecHasher, { - let mut hasher = ProgressiveMerkleHasher::new(); - match T::tree_hash_type() { TreeHashType::Basic => { for item in vec { hasher .write(&item.tree_hash_packed_encoding()) - .expect("ssz_types progressive vec should not exceed the leaf limit"); + .expect("ssz_types vec should not contain more elements than max"); } } TreeHashType::Container | TreeHashType::List | TreeHashType::Vector => { for item in vec { hasher .write(item.tree_hash_root().as_slice()) - .expect("ssz_types progressive vec should not exceed the leaf limit"); + .expect("ssz_types vec should not contain more elements than max"); } } } hasher .finish() - .expect("ssz_types progressive vec should not have a remaining buffer") + .expect("ssz_types vec should not have a remaining buffer") +} + +/// A helper function providing common functionality between the `TreeHash` implementations for +/// `FixedVector` and `VariableList`. +pub fn vec_tree_hash_root(vec: &[T], max_leaves: usize) -> Hash256 +where + T: TreeHash, +{ + let leaves = match T::tree_hash_type() { + TreeHashType::Basic => max_leaves.div_ceil(T::tree_hash_packing_factor()), + TreeHashType::Container | TreeHashType::List | TreeHashType::Vector => max_leaves, + }; + hash_vec(MerkleHasher::with_leaves(leaves), vec) +} + +/// Like `vec_tree_hash_root`, but uses progressive merkleization from EIP-7916. +pub fn progressive_vec_tree_hash_root(vec: &[T]) -> Hash256 +where + T: TreeHash, +{ + hash_vec(ProgressiveMerkleHasher::new(), vec) } diff --git a/src/variable_list.rs b/src/variable_list.rs index b8dab0e..40b0c99 100644 --- a/src/variable_list.rs +++ b/src/variable_list.rs @@ -4,7 +4,6 @@ use serde::Deserialize; use serde_derive::Serialize; use std::any::TypeId; use std::marker::PhantomData; -use std::mem; use std::ops::{Deref, DerefMut, Index, IndexMut}; use std::slice::SliceIndex; use tree_hash::Hash256; @@ -313,9 +312,7 @@ where ))); } - // Safety: We've verified T is u8, so Vec *is* Vec. - let vec_u8 = bytes.to_vec(); - let vec_t = unsafe { mem::transmute::, Vec>(vec_u8) }; + let vec_t = crate::u8_bytes_to_vec(bytes); return Self::new(vec_t).map_err(|e| { ssz::DecodeError::BytesInvalid(format!( "Wrong number of VariableList elements: {:?}", @@ -409,6 +406,7 @@ mod test { use super::*; use ssz::*; use std::collections::HashSet; + use std::mem; use tree_hash::{merkle_root, TreeHash}; use tree_hash_derive::TreeHash; use typenum::*; From 9203d56ad2d7bc5f12133d043e085843f81edcd6 Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 30 Jun 2026 03:21:08 +1000 Subject: [PATCH 3/4] Rename serde helpers to be consistent --- src/serde_utils/mod.rs | 4 ++-- ...of_hex_prog_fixed_vec.rs => prog_list_of_hex_fixed_vec.rs} | 0 ...hex_prog_var_list.rs => prog_list_of_hex_prog_var_list.rs} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/serde_utils/{list_of_hex_prog_fixed_vec.rs => prog_list_of_hex_fixed_vec.rs} (100%) rename src/serde_utils/{list_of_hex_prog_var_list.rs => prog_list_of_hex_prog_var_list.rs} (97%) diff --git a/src/serde_utils/mod.rs b/src/serde_utils/mod.rs index c408084..1b9d0fa 100644 --- a/src/serde_utils/mod.rs +++ b/src/serde_utils/mod.rs @@ -2,8 +2,8 @@ pub mod hex_fixed_vec; pub mod hex_prog_var_list; pub mod hex_var_list; pub mod list_of_hex_fixed_vec; -pub mod list_of_hex_prog_fixed_vec; -pub mod list_of_hex_prog_var_list; pub mod list_of_hex_var_list; +pub mod prog_list_of_hex_fixed_vec; +pub mod prog_list_of_hex_prog_var_list; pub mod quoted_u64_fixed_vec; pub mod quoted_u64_var_list; diff --git a/src/serde_utils/list_of_hex_prog_fixed_vec.rs b/src/serde_utils/prog_list_of_hex_fixed_vec.rs similarity index 100% rename from src/serde_utils/list_of_hex_prog_fixed_vec.rs rename to src/serde_utils/prog_list_of_hex_fixed_vec.rs diff --git a/src/serde_utils/list_of_hex_prog_var_list.rs b/src/serde_utils/prog_list_of_hex_prog_var_list.rs similarity index 97% rename from src/serde_utils/list_of_hex_prog_var_list.rs rename to src/serde_utils/prog_list_of_hex_prog_var_list.rs index 8718c05..5806de5 100644 --- a/src/serde_utils/list_of_hex_prog_var_list.rs +++ b/src/serde_utils/prog_list_of_hex_prog_var_list.rs @@ -68,7 +68,7 @@ mod test { #[derive(Debug, PartialEq, Serialize, Deserialize)] struct Obj { - #[serde(with = "crate::serde_utils::list_of_hex_prog_var_list")] + #[serde(with = "crate::serde_utils::prog_list_of_hex_prog_var_list")] lists: ProgressiveVariableList>, } From 5a49a81f76a2966e85030b10c2f05b4712f8547f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 11 Sep 2026 14:43:37 +1000 Subject: [PATCH 4/4] Reject SSZ bytes larger than 2^32 --- src/progressive_variable_list.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/progressive_variable_list.rs b/src/progressive_variable_list.rs index ab4e22e..853a2d0 100644 --- a/src/progressive_variable_list.rs +++ b/src/progressive_variable_list.rs @@ -18,6 +18,10 @@ use tree_hash::Hash256; /// /// Like `VariableList`, it is backed by a Rust `Vec` and serialized identically to a plain list. /// +/// Known spec divergence: encoding does not enforce the SSZ requirement that the total +/// encoding be less than 2^32 bytes. Callers must ensure this limit is respected. +/// Decoding rejects encodings at or above this limit. +/// /// ## Example /// /// ``` @@ -237,6 +241,12 @@ where } fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() > ssz::MAX_LENGTH_VALUE { + return Err(ssz::DecodeError::BytesInvalid( + "ProgressiveVariableList SSZ encoding must be less than 2^32 bytes".into(), + )); + } + if bytes.is_empty() { return Ok(Self::default()); }