Skip to main content

ssz/
missing.rs

1//! `MissingOr<T>` — first-class summary placeholder for SSZ subtree
2//! substitution.
3//!
4//! Two variants:
5//! * `Materialized(T)` — the full value is present.
6//! * `Missing([u8; 32])` — only the precomputed `hash_tree_root` is known.
7//!
8//! Hash invariant (the load-bearing property):
9//! ```text
10//! Missing(h).hash_tree_root::<D>()      == h
11//! Materialized(t).hash_tree_root::<D>() == t.hash_tree_root::<D>()
12//! ```
13//! No `mix_in_selector` is applied — that would defeat substitution.
14//!
15//! Wire form (jar-specific extension; not standard SSZ):
16//! * byte 0 = `0` + payload bytes (Materialized)
17//! * byte 0 = `1` + 32 raw hash bytes (Missing)
18
19use alloc::vec::Vec;
20use core::fmt;
21use digest::Digest;
22use digest::typenum::U32;
23
24use crate::{BYTES_PER_LENGTH_OFFSET, Decode, DecodeError, Encode, HashTreeRoot, read_slice};
25
26/// Either a fully materialized value or a precomputed hash placeholder.
27#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
28pub enum MissingOr<T> {
29    Materialized(T),
30    Missing([u8; 32]),
31}
32
33impl<T: fmt::Debug> fmt::Debug for MissingOr<T> {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Materialized(t) => f.debug_tuple("Materialized").field(t).finish(),
37            Self::Missing(h) => f.debug_tuple("Missing").field(h).finish(),
38        }
39    }
40}
41
42impl<T: Clone> Clone for MissingOr<T> {
43    fn clone(&self) -> Self {
44        match self {
45            Self::Materialized(t) => Self::Materialized(t.clone()),
46            Self::Missing(h) => Self::Missing(*h),
47        }
48    }
49}
50
51impl<T: PartialEq> PartialEq for MissingOr<T> {
52    fn eq(&self, other: &Self) -> bool {
53        match (self, other) {
54            (Self::Materialized(a), Self::Materialized(b)) => a == b,
55            (Self::Missing(a), Self::Missing(b)) => a == b,
56            _ => false,
57        }
58    }
59}
60
61impl<T: Eq> Eq for MissingOr<T> {}
62
63impl<T: Encode> Encode for MissingOr<T> {
64    fn is_ssz_fixed_len() -> bool {
65        false
66    }
67    fn ssz_fixed_len() -> usize {
68        BYTES_PER_LENGTH_OFFSET
69    }
70    fn ssz_bytes_len(&self) -> usize {
71        1 + match self {
72            Self::Materialized(t) => t.ssz_bytes_len(),
73            Self::Missing(_) => 32,
74        }
75    }
76    fn ssz_append(&self, buf: &mut Vec<u8>) {
77        match self {
78            Self::Materialized(t) => {
79                buf.push(0);
80                t.ssz_append(buf);
81            }
82            Self::Missing(h) => {
83                buf.push(1);
84                buf.extend_from_slice(h);
85            }
86        }
87    }
88}
89
90impl<T: Decode> Decode for MissingOr<T> {
91    fn is_ssz_fixed_len() -> bool {
92        false
93    }
94    fn ssz_fixed_len() -> usize {
95        BYTES_PER_LENGTH_OFFSET
96    }
97    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
98        let tag = read_slice(bytes, 0, 1)?[0];
99        match tag {
100            0 => Ok(Self::Materialized(T::from_ssz_bytes(&bytes[1..])?)),
101            1 => {
102                if bytes.len() != 33 {
103                    return Err(DecodeError::TrailingBytes {
104                        expected: 33,
105                        actual: bytes.len(),
106                    });
107                }
108                let mut h = [0u8; 32];
109                h.copy_from_slice(&bytes[1..33]);
110                Ok(Self::Missing(h))
111            }
112            v => Err(DecodeError::InvalidSelector(v)),
113        }
114    }
115}
116
117impl<T: HashTreeRoot> HashTreeRoot for MissingOr<T> {
118    fn hash_tree_root<D: Digest<OutputSize = U32>>(&self) -> [u8; 32] {
119        // CRITICAL: no mix_in_selector. Substitution requires identity.
120        match self {
121            Self::Materialized(t) => t.hash_tree_root::<D>(),
122            Self::Missing(h) => *h,
123        }
124    }
125}