ssz/lib.rs
1//! SimpleSerialize (SSZ) codec with `hash_tree_root`.
2//!
3//! Implements the Ethereum consensus SSZ wire format plus two jar-specific
4//! extensions ([`MissingOr`], [`SparseList`]) that allow precomputed subtree
5//! roots to substitute transparently for materialized leaves.
6//!
7//! The default hash function is SHA-256 (via the optional `sha2` feature);
8//! the [`HashTreeRoot`] trait is generic over any `digest::Digest` with a
9//! 32-byte output, so callers can plug in alternative hashes at compile time.
10//!
11//! # Wire format
12//!
13//! See [`encoding`](https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md)
14//! for the spec we implement. Notable deviations:
15//!
16//! - [`Option`] / SSZ Union: byte 0 = None (no payload), byte 1 = Some(T) + T's bytes.
17//! - [`MissingOr`]: byte 0 = Materialized + T's bytes, byte 1 = Missing + 32 raw bytes.
18//! - [`SparseList`]: same wire format as a `List<T, N>` plus a length prefix.
19
20#![cfg_attr(not(feature = "std"), no_std)]
21extern crate alloc;
22
23use alloc::vec::Vec;
24use digest::Digest;
25use digest::typenum::U32;
26
27pub mod bits;
28pub mod collections;
29mod error;
30pub mod list;
31pub mod merkle;
32pub mod missing;
33pub mod primitives;
34pub mod sparse;
35pub mod union;
36pub mod vector;
37
38pub use bits::{Bitlist, Bitvector};
39// Re-exports so consumers of the derive macros can name the crates the
40// generated code references without taking direct dependencies.
41pub use digest;
42
43/// Hidden re-exports used by the derive macros. Not part of the public
44/// API; do not depend on this directly.
45#[doc(hidden)]
46pub mod __private {
47 pub use alloc::vec::Vec;
48}
49pub use error::DecodeError;
50pub use list::List;
51pub use merkle::{merkleize, mix_in_length, mix_in_selector, pack_bytes, zero_hash};
52pub use missing::MissingOr;
53pub use primitives::U256;
54pub use sparse::SparseList;
55pub use vector::FixedVector;
56
57#[cfg(feature = "derive")]
58pub use ssz_derive::{Decode, Encode, HashTreeRoot};
59
60/// The number of bytes used to encode a variable-length list offset.
61///
62/// SSZ fixes this at 4 (a little-endian `u32`).
63pub const BYTES_PER_LENGTH_OFFSET: usize = 4;
64
65/// Chunk size in bytes for SSZ merkleization.
66pub const BYTES_PER_CHUNK: usize = 32;
67
68/// SSZ encoding trait.
69///
70/// `ssz_append` is the primary primitive: every other method delegates to it.
71pub trait Encode {
72 /// `true` iff this type is fixed-length (no variable-length fields).
73 fn is_ssz_fixed_len() -> bool;
74
75 /// Number of bytes this type occupies in the fixed-length portion of a
76 /// container encoding. For variable-length types this returns
77 /// [`BYTES_PER_LENGTH_OFFSET`] (i.e. the size of the offset slot).
78 fn ssz_fixed_len() -> usize {
79 BYTES_PER_LENGTH_OFFSET
80 }
81
82 /// `true` for "basic" SSZ types (uintN, bool), which pack adjacent
83 /// elements into shared 32-byte chunks for merkleization. Composite
84 /// types (containers, lists, structs) return `false` (the default).
85 fn is_basic_type() -> bool {
86 false
87 }
88
89 /// Total size of `self` when serialized.
90 fn ssz_bytes_len(&self) -> usize;
91
92 /// Append the encoding of `self` to `buf`.
93 fn ssz_append(&self, buf: &mut Vec<u8>);
94
95 /// Serialize into a fresh `Vec<u8>` allocated through the global allocator.
96 fn as_ssz_bytes(&self) -> Vec<u8> {
97 let mut v = Vec::with_capacity(self.ssz_bytes_len());
98 self.ssz_append(&mut v);
99 v
100 }
101}
102
103/// SSZ decoding trait.
104pub trait Decode: Sized {
105 /// `true` iff this type is fixed-length.
106 fn is_ssz_fixed_len() -> bool;
107
108 /// Number of bytes this type occupies in the fixed-length portion of a
109 /// container encoding. Variable-length types return
110 /// [`BYTES_PER_LENGTH_OFFSET`].
111 fn ssz_fixed_len() -> usize {
112 BYTES_PER_LENGTH_OFFSET
113 }
114
115 /// Decode a full instance from `bytes`, rejecting trailing input.
116 fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError>;
117}
118
119/// Computes a 32-byte hash tree root for SSZ types.
120///
121/// Generic over the hash function so callers can plug in SHA-256, Blake2b,
122/// etc. Requires `OutputSize = U32`, i.e. a 32-byte digest.
123pub trait HashTreeRoot {
124 /// Compute the hash tree root using `D` as the underlying hash.
125 fn hash_tree_root<D: Digest<OutputSize = U32>>(&self) -> [u8; 32];
126}
127
128/// Convenience SHA-256 entry point.
129///
130/// Rust forbids default type parameters on free functions, so this is the
131/// SHA-256-specialised companion to [`HashTreeRoot::hash_tree_root`].
132#[cfg(feature = "sha2")]
133pub fn hash_tree_root<T: HashTreeRoot + ?Sized>(value: &T) -> [u8; 32] {
134 value.hash_tree_root::<sha2::Sha256>()
135}
136
137// --------------------------------------------------------------------------
138// Internal helpers
139// --------------------------------------------------------------------------
140
141/// Wraps a slice index check that returns [`DecodeError::UnexpectedEof`] on
142/// out-of-bounds.
143#[inline]
144pub(crate) fn read_slice(bytes: &[u8], offset: usize, len: usize) -> Result<&[u8], DecodeError> {
145 let end = offset.checked_add(len).ok_or(DecodeError::UnexpectedEof {
146 expected: len,
147 actual: bytes.len().saturating_sub(offset),
148 })?;
149 if end > bytes.len() {
150 return Err(DecodeError::UnexpectedEof {
151 expected: len,
152 actual: bytes.len().saturating_sub(offset),
153 });
154 }
155 Ok(&bytes[offset..end])
156}
157
158/// Reads a little-endian u32 length offset from `bytes[off..off+4]`.
159#[inline]
160pub(crate) fn read_offset(bytes: &[u8], off: usize) -> Result<usize, DecodeError> {
161 let slice = read_slice(bytes, off, BYTES_PER_LENGTH_OFFSET)?;
162 let arr: [u8; 4] = slice.try_into().expect("len checked");
163 Ok(u32::from_le_bytes(arr) as usize)
164}