From 2f1a8bb1f1bef184006122b55935e1e28d9db307 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Mon, 16 Mar 2026 16:59:55 +0100 Subject: [PATCH] add list encoded option --- src/lib.rs | 4 + src/list_encoded_option.rs | 221 +++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/list_encoded_option.rs diff --git a/src/lib.rs b/src/lib.rs index 1656587..967562d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,10 @@ mod runtime_types; #[cfg(feature = "runtime_types")] pub use runtime_types::{RuntimeFixedVector, RuntimeVariableList}; +mod list_encoded_option; + +pub use list_encoded_option::ListEncodedOption; + /// Returned when an item encounters an error. #[derive(PartialEq, Debug, Clone)] pub enum Error { diff --git a/src/list_encoded_option.rs b/src/list_encoded_option.rs new file mode 100644 index 0000000..214a608 --- /dev/null +++ b/src/list_encoded_option.rs @@ -0,0 +1,221 @@ +use crate::tree_hash::vec_tree_hash_root; +use crate::VariableList; +use serde_derive::{Deserialize, Serialize}; +use smallvec::SmallVec; +use ssz::{Decode, DecodeError, Encode}; +use std::ops::{Deref, DerefMut}; +use tree_hash::{Hash256, PackedEncoding, TreeHash, TreeHashType}; +use typenum::U1; + +/// An `Option` that is SSZ-encoded as a `VariableList`. +/// +/// `None` is encoded as an empty list and `Some(value)` as a single-element list. +/// This is useful for representing optional fields in SSZ containers without +/// relying on a union type. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ListEncodedOption(pub Option); + +impl Deref for ListEncodedOption { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ListEncodedOption { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for ListEncodedOption { + fn from(option: Option) -> Self { + Self(option) + } +} + +impl From> for Option { + fn from(option: ListEncodedOption) -> Self { + option.0 + } +} + +impl Encode for ListEncodedOption { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + if let Some(t) = &self.0 { + SmallVec::from_buf([t]).ssz_append(buf) + } + } + + fn ssz_bytes_len(&self) -> usize { + if let Some(t) = &self.0 { + SmallVec::from_buf([t]).ssz_bytes_len() + } else { + 0 + } + } +} + +impl Decode for ListEncodedOption { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let list = VariableList::::from_ssz_bytes(bytes)?; + Ok(Self(list.into_iter().next())) + } +} + +impl TreeHash for ListEncodedOption { + fn tree_hash_type() -> TreeHashType { + 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 slice: &[T] = match &self.0 { + Some(val) => std::slice::from_ref(val), + None => &[], + }; + let root = vec_tree_hash_root::(slice, 1); + tree_hash::mix_in_length(&root, slice.len()) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a, T: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a> for ListEncodedOption { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self(Option::arbitrary(u)?)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use ssz::{Decode, Encode}; + use typenum::U1; + + #[test] + fn none_encodes_as_empty_list() { + let option: ListEncodedOption = None.into(); + let list: VariableList = VariableList::try_from(vec![]).unwrap(); + assert_eq!(option.as_ssz_bytes(), list.as_ssz_bytes()); + assert_eq!(option.ssz_bytes_len(), list.ssz_bytes_len()); + } + + #[test] + fn some_encodes_as_single_element_list() { + let option: ListEncodedOption = Some(42u64).into(); + let list: VariableList = VariableList::try_from(vec![42u64]).unwrap(); + assert_eq!(option.as_ssz_bytes(), list.as_ssz_bytes()); + assert_eq!(option.ssz_bytes_len(), list.ssz_bytes_len()); + } + + #[test] + fn round_trip_none() { + let original: ListEncodedOption = None.into(); + let bytes = original.as_ssz_bytes(); + let decoded = ListEncodedOption::::from_ssz_bytes(&bytes).unwrap(); + assert!(decoded.is_none()); + } + + #[test] + fn round_trip_some() { + let original: ListEncodedOption = Some(123u64).into(); + let bytes = original.as_ssz_bytes(); + let decoded = ListEncodedOption::::from_ssz_bytes(&bytes).unwrap(); + assert_eq!(*decoded, Some(123u64)); + } + + #[test] + fn none_variable_encodes_as_empty_list() { + let option: ListEncodedOption> = None.into(); + let list: VariableList, U1> = VariableList::try_from(vec![]).unwrap(); + assert_eq!(option.as_ssz_bytes(), list.as_ssz_bytes()); + assert_eq!(option.ssz_bytes_len(), list.ssz_bytes_len()); + } + + #[test] + fn some_variable_encodes_as_single_element_list() { + let value = vec![1u8, 2, 3]; + let option: ListEncodedOption> = Some(value.clone()).into(); + let list: VariableList, U1> = VariableList::try_from(vec![value]).unwrap(); + assert_eq!(option.as_ssz_bytes(), list.as_ssz_bytes()); + assert_eq!(option.ssz_bytes_len(), list.ssz_bytes_len()); + } + + #[test] + fn round_trip_variable_none() { + let original: ListEncodedOption> = None.into(); + let bytes = original.as_ssz_bytes(); + let decoded = ListEncodedOption::>::from_ssz_bytes(&bytes).unwrap(); + assert!(decoded.is_none()); + } + + #[test] + fn round_trip_variable_some() { + let original: ListEncodedOption> = Some(vec![4u8, 5, 6]).into(); + let bytes = original.as_ssz_bytes(); + let decoded = ListEncodedOption::>::from_ssz_bytes(&bytes).unwrap(); + assert_eq!(*decoded, Some(vec![4u8, 5, 6])); + } + + #[test] + fn rejects_two_element_list() { + use typenum::U2; + let list: VariableList = VariableList::try_from(vec![1u64, 2]).unwrap(); + assert!(ListEncodedOption::::from_ssz_bytes(&list.as_ssz_bytes()).is_err()); + } + + #[test] + fn is_variable_len() { + assert!(! as Encode>::is_ssz_fixed_len()); + assert!(! as Decode>::is_ssz_fixed_len()); + } + + #[test] + fn tree_hash_none_matches_empty_list() { + let option: ListEncodedOption = None.into(); + let list: VariableList = VariableList::try_from(vec![]).unwrap(); + assert_eq!(option.tree_hash_root(), list.tree_hash_root()); + } + + #[test] + fn tree_hash_some_matches_single_element_list() { + let option: ListEncodedOption = Some(42u64).into(); + let list: VariableList = VariableList::try_from(vec![42u64]).unwrap(); + assert_eq!(option.tree_hash_root(), list.tree_hash_root()); + } + + #[test] + fn tree_hash_variable_none_matches_empty_list() { + let option: ListEncodedOption> = None.into(); + let list: VariableList, U1> = + VariableList::try_from(vec![]).unwrap(); + assert_eq!(option.tree_hash_root(), list.tree_hash_root()); + } + + #[test] + fn tree_hash_variable_some_matches_single_element_list() { + let value: VariableList = + VariableList::try_from(vec![1u8, 2, 3]).unwrap(); + let option: ListEncodedOption> = Some(value.clone()).into(); + let list: VariableList, U1> = + VariableList::try_from(vec![value]).unwrap(); + assert_eq!(option.tree_hash_root(), list.tree_hash_root()); + } +}