Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSZ Optionals #13

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ itertools = "0.10.0"
[dev-dependencies]
serde_json = "1.0.0"
tree_hash_derive = "0.5.0"
ethereum_ssz_derive = "0.5.0"
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@
#[macro_use]
mod bitfield;
mod fixed_vector;
mod optional;
pub mod serde_utils;
mod tree_hash;
mod variable_list;

pub use bitfield::{BitList, BitVector, Bitfield};
pub use fixed_vector::FixedVector;
pub use optional::Optional;
pub use typenum;
pub use variable_list::VariableList;

Expand Down
251 changes: 251 additions & 0 deletions src/optional.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
use crate::tree_hash::optional_tree_hash_root;
use derivative::Derivative;
use serde_derive::{Deserialize, Serialize};
use tree_hash::Hash256;

pub use typenum;

/// Emulates a SSZ `Optional` (distinct from a Rust `Option`).
///
/// This SSZ type is defined in EIP-6475.
///
/// This struct is backed by a Rust `Option` and its behaviour is defined by the variant.
///
/// If `Some`, it will serialize with a 1-byte identifying prefix with a value of 1 followed by the
/// serialized internal type.
/// If `None`, it will serialize as `null`.
///
/// `Optional` will Merklize in the following ways:
/// `if None`: Merklize as an empty `VariableList`
/// `if Some(T)`: Merklize as a `VariableList` of length 1 whose single value is `T`.
///
/// ## Example
///
/// ```
/// use ssz_types::{Optional, typenum::*, VariableList};
/// use tree_hash::TreeHash;
/// use ssz::Encode;
///
/// // Create an `Optional` from an `Option` that is `Some`.
/// let some: Option<u8> = Some(9);
/// let ssz: Optional<u8> = Optional::from(some);
/// let serialized: &[u8] = &ssz.as_ssz_bytes();
/// assert_eq!(serialized, &[1, 9]);
///
/// let root = ssz.tree_hash_root();
/// let equivalent_list: VariableList<u64, U1> = VariableList::from(vec![9; 1]);
/// assert_eq!(root, equivalent_list.tree_hash_root());
///
/// // Create an `Optional` from an `Option` that is `None`.
/// let none: Option<u8> = None;
/// let ssz: Optional<u8> = Optional::from(none);
/// let serialized: &[u8] = &ssz.as_ssz_bytes();
/// let null: &[u8] = &[];
/// assert_eq!(serialized, null);
///
/// let root = ssz.tree_hash_root();
/// let equivalent_list: VariableList<u8, U0> = VariableList::from(vec![]);
/// assert_eq!(root, equivalent_list.tree_hash_root());
///
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, Derivative)]
#[derivative(PartialEq, Hash(bound = "T: std::hash::Hash"))]
#[serde(transparent)]
pub struct Optional<T> {
optional: Option<T>,
}

impl<T> From<Option<T>> for Optional<T> {
fn from(optional: Option<T>) -> Self {
Self { optional }
}
}

impl<T> From<Optional<T>> for Option<T> {
fn from(val: Optional<T>) -> Option<T> {
val.optional
}
}

impl<T> Default for Optional<T> {
fn default() -> Self {
Self { optional: None }
}
}

impl<T> tree_hash::TreeHash for Optional<T>
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 = optional_tree_hash_root::<T>(&self.optional);

let length = match &self.optional {
None => 0,
Some(_) => 1,
};

tree_hash::mix_in_length(&root, length)
}
}

impl<T> ssz::Encode for Optional<T>
where
T: ssz::Encode,
{
fn is_ssz_fixed_len() -> bool {
false
}

fn ssz_bytes_len(&self) -> usize {
match &self.optional {
None => 0,
Some(val) => val.ssz_bytes_len() + 1,
}
}

fn ssz_append(&self, buf: &mut Vec<u8>) {
match &self.optional {
None => (),
Some(val) => {
let mut optional_identifier = vec![1];
buf.append(&mut optional_identifier);
val.ssz_append(buf)
}
}
}
}

impl<T> ssz::Decode for Optional<T>
where
T: ssz::Decode,
{
fn is_ssz_fixed_len() -> bool {
false
}

fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
if let Some((first, rest)) = bytes.split_first() {
if first == &0x01 {
return Ok(Optional {
optional: Some(T::from_ssz_bytes(&rest)?),
});
} else {
// An `Optional` must always contains `0x01` as the first byte.
// Might be worth having an explicit error variant in ssz::DecodeError.
return Err(ssz::DecodeError::BytesInvalid(
"Missing Optional identifier byte".to_string(),
));
}
} else {
Ok(Optional { optional: None })
}
}
}

/// TODO Use a more robust `Arbitrary` impl.
#[cfg(feature = "arbitrary")]
impl<'a, T: arbitrary::Arbitrary<'a>> arbitrary::Arbitrary<'a> for Optional<T> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let option = Some(<T>::arbitrary(u).unwrap());
Ok(Self::from(option))
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::{FixedVector, VariableList};
use ssz::*;
use ssz_derive::{Decode, Encode};
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
use typenum::*;

#[test]
fn encode() {
let some: Optional<u8> = Some(42).into();
let bytes: Vec<u8> = vec![1, 42];
assert_eq!(some.as_ssz_bytes(), bytes);

let none: Optional<u8> = None.into();
let empty: Vec<u8> = vec![];
assert_eq!(none.as_ssz_bytes(), empty);
}

#[test]
fn decode() {
let bytes = &[1, 42, 0, 0, 0, 0, 0, 0, 0];
let some: Optional<u64> = Optional::from_ssz_bytes(bytes).unwrap();
assert_eq!(Some(42), some.optional);

let empty = &[];
let none: Optional<u64> = Optional::from_ssz_bytes(empty).unwrap();
assert_eq!(None, none.optional);
}

#[test]
fn tree_hash_none() {
// None should merklize the same as an empty VariableList.
let none: Optional<u8> = Optional::from(None);
let empty_list: VariableList<u8, U0> = VariableList::from(vec![]);
assert_eq!(none.tree_hash_root(), empty_list.tree_hash_root());
}

#[test]
fn tree_hash_some_int() {
// Optional should merklize the same as a length 1 VariableList.
let some_int: Optional<u8> = Optional::from(Some(9));
let list_int: VariableList<u8, U1> = VariableList::from(vec![9; 1]);
assert_eq!(some_int.tree_hash_root(), list_int.tree_hash_root());
}

#[test]
fn tree_hash_some_list() {
// Optional should merklize the same as a length 1 VariableList.
let list: VariableList<u8, U16> = VariableList::from(vec![9; 16]);
let some_list: Optional<VariableList<u8, U16>> = Optional::from(Some(list.clone()));
let list_list: VariableList<VariableList<u8, U16>, U1> = VariableList::from(vec![list; 1]);
assert_eq!(some_list.tree_hash_root(), list_list.tree_hash_root());
}

#[test]
fn tree_hash_some_vec() {
// Optional should merklize the same as a length 1 VariableList.
let vec: FixedVector<u8, U16> = FixedVector::from(vec![9; 16]);
let some_vec: Optional<FixedVector<u8, U16>> = Optional::from(Some(vec.clone()));
let list_vec: VariableList<FixedVector<u8, U16>, U1> = VariableList::from(vec![vec; 1]);
assert_eq!(some_vec.tree_hash_root(), list_vec.tree_hash_root());
}

#[test]
fn tree_hash_some_object() {
#[derive(TreeHash, Decode, Encode)]
struct Object {
a: u8,
b: u8,
}

// Optional should merklize the same as a length 1 VariableList. Note the 1-byte identifier
// during deserialization.
let optional_object: Optional<Object> = Optional::from_ssz_bytes(&[1, 11, 9]).unwrap();
let list_object: VariableList<Object, U1> = VariableList::from_ssz_bytes(&[11, 9]).unwrap();

assert_eq!(
optional_object.tree_hash_root(),
list_object.tree_hash_root()
);
}
}
19 changes: 19 additions & 0 deletions src/tree_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,22 @@ pub fn bitfield_bytes_tree_hash_root<N: Unsigned>(bytes: &[u8]) -> Hash256 {
.finish()
.expect("bitfield tree hash buffer should not exceed leaf limit")
}

/// A helper function providing common functionality for finding the Merkle root of some bytes that
/// represent an optional value.
pub fn optional_tree_hash_root<T: TreeHash>(option: &Option<T>) -> Hash256 {
let mut hasher = MerkleHasher::with_leaves(1);

match option {
None => (),
Some(val) => {
hasher
.write(val.tree_hash_root().as_bytes())
.expect("ssz_types optional should only contain 1 element");
}
}

hasher
.finish()
.expect("ssz_types optional should not have a remaining buffer")
}