From d825a64feca5d35c74981a893fd3bcea676acf09 Mon Sep 17 00:00:00 2001 From: Mac L Date: Fri, 3 Oct 2025 22:41:33 +1000 Subject: [PATCH] Add runtime_types --- .github/workflows/test-suite.yml | 2 +- Cargo.toml | 8 + src/lib.rs | 14 +- src/runtime_types/mod.rs | 5 + src/runtime_types/runtime_fixed_vector.rs | 90 +++++ src/runtime_types/runtime_variable_list.rs | 387 +++++++++++++++++++++ 6 files changed, 501 insertions(+), 5 deletions(-) create mode 100644 src/runtime_types/mod.rs create mode 100644 src/runtime_types/runtime_fixed_vector.rs create mode 100644 src/runtime_types/runtime_variable_list.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index f5688e5..5b973cb 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -30,7 +30,7 @@ jobs: - name: Get latest version of stable Rust run: rustup update stable - name: Run tests - run: cargo test --release + run: cargo test --release --all-features coverage: name: cargo-tarpaulin runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 4a7b3ca..9212966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,19 @@ smallvec = "1.8.0" arbitrary = { version = "1.0", features = ["derive"], optional = true } itertools = "0.14.0" +# Optional dependencies +educe = { version = "0.6", optional = true } +# Use crates.io version once published. +context_deserialize = { git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0", optional = true } + [dev-dependencies] criterion = "0.7.0" serde_json = "1.0.0" tree_hash_derive = "0.10.0" +[features] +runtime-types = ["dep:context_deserialize", "dep:educe"] + [[bench]] harness = false name = "encode_decode" diff --git a/src/lib.rs b/src/lib.rs index fdff006..dc271cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,9 +37,13 @@ //! //! ``` +pub mod serde_utils; +pub mod length { + pub use ssz::{Fixed, Variable}; +} + #[macro_use] mod fixed_vector; -pub mod serde_utils; mod tree_hash; mod variable_list; @@ -48,9 +52,11 @@ pub use ssz::{BitList, BitVector, Bitfield}; pub use typenum; pub use variable_list::VariableList; -pub mod length { - pub use ssz::{Fixed, Variable}; -} +#[cfg(feature = "runtime-types")] +mod runtime_types; + +#[cfg(feature = "runtime-types")] +pub use runtime_types::{RuntimeFixedVector, RuntimeVariableList}; /// Returned when an item encounters an error. #[derive(PartialEq, Debug, Clone)] diff --git a/src/runtime_types/mod.rs b/src/runtime_types/mod.rs new file mode 100644 index 0000000..b88e0d3 --- /dev/null +++ b/src/runtime_types/mod.rs @@ -0,0 +1,5 @@ +mod runtime_fixed_vector; +mod runtime_variable_list; + +pub use runtime_fixed_vector::RuntimeFixedVector; +pub use runtime_variable_list::RuntimeVariableList; diff --git a/src/runtime_types/runtime_fixed_vector.rs b/src/runtime_types/runtime_fixed_vector.rs new file mode 100644 index 0000000..f562322 --- /dev/null +++ b/src/runtime_types/runtime_fixed_vector.rs @@ -0,0 +1,90 @@ +//! Emulates a fixed size array but with the length set at runtime. +//! +//! The length of the list cannot be changed once it is set. + +use std::fmt; +use std::fmt::Debug; + +#[derive(Clone)] +pub struct RuntimeFixedVector { + vec: Vec, + len: usize, +} + +impl Debug for RuntimeFixedVector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?} (len={})", self.vec, self.len) + } +} + +impl RuntimeFixedVector { + pub fn new(vec: Vec) -> Self { + let len = vec.len(); + Self { vec, len } + } + + pub fn to_vec(&self) -> Vec { + self.vec.clone() + } + + pub fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.len + } + + pub fn into_vec(self) -> Vec { + self.vec + } + + pub fn default(max_len: usize) -> Self { + Self { + vec: vec![T::default(); max_len], + len: max_len, + } + } + + pub fn take(&mut self) -> Self { + let new = std::mem::take(&mut self.vec); + *self = Self::new(vec![T::default(); self.len]); + Self { + vec: new, + len: self.len, + } + } +} + +impl std::ops::Deref for RuntimeFixedVector { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + +impl std::ops::DerefMut for RuntimeFixedVector { + fn deref_mut(&mut self) -> &mut [T] { + &mut self.vec[..] + } +} + +impl IntoIterator for RuntimeFixedVector { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl<'a, T> IntoIterator for &'a RuntimeFixedVector { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.vec.iter() + } +} diff --git a/src/runtime_types/runtime_variable_list.rs b/src/runtime_types/runtime_variable_list.rs new file mode 100644 index 0000000..0d79c89 --- /dev/null +++ b/src/runtime_types/runtime_variable_list.rs @@ -0,0 +1,387 @@ +use crate::Error; +use context_deserialize::ContextDeserialize; +use educe::Educe; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; +use std::fmt; +use std::fmt::Debug; +use std::ops::{Deref, Index, IndexMut}; +use std::slice::SliceIndex; +use tree_hash::{Hash256, MerkleHasher, PackedEncoding, TreeHash, TreeHashType}; + +/// Emulates a SSZ `List`. +/// +/// An ordered, heap-allocated, variable-length, homogeneous collection of `T`, with no more than +/// `max_len` values. +/// +/// To ensure there are no inconsistent states, we do not allow any mutating operation if `max_len` is not set. +/// +/// ## Example +/// +/// ``` +/// use ssz_types::RuntimeVariableList; +/// +/// let base: Vec = vec![1, 2, 3, 4]; +/// +/// // Create a `RuntimeVariableList` from a `Vec` that has the expected length. +/// let exact: RuntimeVariableList<_> = RuntimeVariableList::new(base.clone(), 4).unwrap(); +/// assert_eq!(&exact[..], &[1, 2, 3, 4]); +/// +/// // Create a `RuntimeVariableList` from a `Vec` that is too long you'll get an error. +/// let err = RuntimeVariableList::new(base.clone(), 3).unwrap_err(); +/// assert_eq!(err, ssz_types::Error::OutOfBounds { i: 4, len: 3 }); +/// +/// // Create a `RuntimeVariableList` from a `Vec` that is shorter than the maximum. +/// let mut long: RuntimeVariableList<_> = RuntimeVariableList::new(base, 5).unwrap(); +/// assert_eq!(&long[..], &[1, 2, 3, 4]); +/// +/// // Push a value to if it does not exceed the maximum +/// long.push(5).unwrap(); +/// assert_eq!(&long[..], &[1, 2, 3, 4, 5]); +/// +/// // Push a value to if it _does_ exceed the maximum. +/// assert!(long.push(6).is_err()); +/// +/// ``` +#[derive(Clone, Serialize, Deserialize, Educe)] +#[educe(PartialEq, Eq, Hash(bound = "T: std::hash::Hash"))] +#[serde(transparent)] +pub struct RuntimeVariableList { + vec: Vec, + #[serde(skip)] + max_len: usize, +} + +impl Debug for RuntimeVariableList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?} (max_len={})", self.vec, self.max_len) + } +} + +impl RuntimeVariableList { + /// Returns `Ok` if the given `vec` equals the fixed length of `Self`. Otherwise returns + /// `Err(OutOfBounds { .. })`. + pub fn new(vec: Vec, max_len: usize) -> Result { + if vec.len() <= max_len { + Ok(Self { vec, max_len }) + } else { + Err(Error::OutOfBounds { + i: vec.len(), + len: max_len, + }) + } + } + + /// Create an empty list with the given `max_len`. + pub fn empty(max_len: usize) -> Self { + Self { + vec: vec![], + max_len, + } + } + + pub fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } + + pub fn as_mut_slice(&mut self) -> &mut [T] { + self.vec.as_mut_slice() + } + + /// 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.len() == 0 + } + + /// Returns the type-level maximum length. + /// + /// Returns `None` if self is uninitialized with a max_len. + pub fn max_len(&self) -> usize { + self.max_len + } + + /// Appends `value` to the back of `self`. + /// + /// Returns `Err(())` when appending `value` would exceed the maximum length. + pub fn push(&mut self, value: T) -> Result<(), Error> { + if self.vec.len() < self.max_len { + self.vec.push(value); + Ok(()) + } else { + Err(Error::OutOfBounds { + i: self.vec.len().saturating_add(1), + len: self.max_len, + }) + } + } +} + +impl RuntimeVariableList { + pub fn from_ssz_bytes(bytes: &[u8], max_len: usize) -> Result { + let vec = if bytes.is_empty() { + vec![] + } else if ::is_ssz_fixed_len() { + let num_items = bytes + .len() + .checked_div(::ssz_fixed_len()) + .ok_or(ssz::DecodeError::ZeroLengthItem)?; + + if num_items > max_len { + return Err(ssz::DecodeError::BytesInvalid(format!( + "RuntimeVariableList of {} items exceeds maximum of {}", + num_items, max_len + ))); + } + + bytes.chunks(::ssz_fixed_len()).try_fold( + Vec::with_capacity(num_items), + |mut vec, chunk| { + vec.push(::from_ssz_bytes(chunk)?); + Ok(vec) + }, + )? + } else { + ssz::decode_list_of_variable_length_items(bytes, Some(max_len))? + }; + Ok(Self { vec, max_len }) + } +} + +impl From> for Vec { + fn from(list: RuntimeVariableList) -> Vec { + list.vec + } +} + +impl> Index for RuntimeVariableList { + type Output = I::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + Index::index(&self.vec, index) + } +} + +impl> IndexMut for RuntimeVariableList { + #[inline] + fn index_mut(&mut self, index: I) -> &mut Self::Output { + IndexMut::index_mut(&mut self.vec, index) + } +} + +impl Deref for RuntimeVariableList { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + +impl<'a, T> IntoIterator for &'a RuntimeVariableList { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for RuntimeVariableList { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl ssz::Encode for RuntimeVariableList +where + T: ssz::Encode, +{ + fn is_ssz_fixed_len() -> bool { + >::is_ssz_fixed_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.vec.ssz_append(buf) + } + + fn ssz_fixed_len() -> usize { + >::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.vec.ssz_bytes_len() + } +} + +impl<'de, C, T> ContextDeserialize<'de, (C, usize)> for RuntimeVariableList +where + T: ContextDeserialize<'de, C>, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: (C, usize)) -> Result + where + D: Deserializer<'de>, + { + // first parse out a Vec using the Vec impl you already have + let vec: Vec = Vec::context_deserialize(deserializer, context.0)?; + let vec_len = vec.len(); + RuntimeVariableList::new(vec, context.1).map_err(|e| { + DeError::custom(format!( + "RuntimeVariableList length {} exceeds max_len {}: {e:?}", + vec_len, context.1, + )) + }) + } +} + +impl TreeHash for RuntimeVariableList { + fn tree_hash_type() -> tree_hash::TreeHashType { + tree_hash::TreeHashType::List + } + + fn tree_hash_packed_encoding(&self) -> 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 = runtime_vec_tree_hash_root::(&self.vec, self.max_len); + + tree_hash::mix_in_length(&root, self.len()) + } +} + +// We can delete this once the upstream `vec_tree_hash_root` is modified to use a runtime max len. +pub fn runtime_vec_tree_hash_root(vec: &[T], max_len: usize) -> Hash256 +where + T: TreeHash, +{ + match T::tree_hash_type() { + TreeHashType::Basic => { + let mut hasher = + MerkleHasher::with_leaves(max_len.div_ceil(T::tree_hash_packing_factor())); + + for item in vec { + hasher + .write(&item.tree_hash_packed_encoding()) + .expect("ssz_types variable vec should not contain more elements than max"); + } + + 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_len); + + for item in vec { + hasher + .write(item.tree_hash_root().as_slice()) + .expect("ssz_types vec should not contain more elements than max"); + } + + hasher + .finish() + .expect("ssz_types vec should not have a remaining buffer") + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use ssz::*; + use std::fmt::Debug; + + #[test] + fn new() { + let vec = vec![42; 5]; + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_err()); + + let vec = vec![42; 3]; + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_ok()); + + let vec = vec![42; 4]; + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_ok()); + } + + #[test] + fn indexing() { + let vec = vec![1, 2]; + + let mut fixed: RuntimeVariableList = + RuntimeVariableList::new(vec.clone(), 8192).unwrap(); + + assert_eq!(fixed[0], 1); + assert_eq!(&fixed[0..1], &vec[0..1]); + assert_eq!(fixed[..].len(), 2); + + fixed[1] = 3; + assert_eq!(fixed[1], 3); + } + + #[test] + fn length() { + // Too long. + let vec = vec![42; 5]; + let err = RuntimeVariableList::::new(vec.clone(), 4).unwrap_err(); + assert_eq!(err, Error::OutOfBounds { i: 5, len: 4 }); + + let vec = vec![42; 3]; + let fixed: RuntimeVariableList = RuntimeVariableList::new(vec.clone(), 4).unwrap(); + assert_eq!(&fixed[0..3], &vec[..]); + assert_eq!(&fixed[..], &vec![42, 42, 42][..]); + + let vec = vec![]; + let fixed: RuntimeVariableList = RuntimeVariableList::new(vec, 4).unwrap(); + assert_eq!(&fixed[..], &[] as &[u64]); + } + + #[test] + fn deref() { + let vec = vec![0, 2, 4, 6]; + let fixed: RuntimeVariableList = RuntimeVariableList::new(vec, 4).unwrap(); + + assert_eq!(fixed.first(), Some(&0)); + assert_eq!(fixed.get(3), Some(&6)); + assert_eq!(fixed.get(4), None); + } + + #[test] + fn encode() { + let vec: RuntimeVariableList = RuntimeVariableList::new(vec![0; 2], 2).unwrap(); + assert_eq!(vec.as_ssz_bytes(), vec![0, 0, 0, 0]); + assert_eq!( as Encode>::ssz_fixed_len(), 4); + } + + fn round_trip(item: RuntimeVariableList) { + let max_len = item.max_len(); + let encoded = &item.as_ssz_bytes(); + assert_eq!(item.ssz_bytes_len(), encoded.len()); + assert_eq!( + RuntimeVariableList::from_ssz_bytes(encoded, max_len), + Ok(item) + ); + } + + #[test] + fn u16_len_8() { + round_trip::(RuntimeVariableList::new(vec![42; 8], 8).unwrap()); + round_trip::(RuntimeVariableList::new(vec![0; 8], 8).unwrap()); + } +}