Skip to main content

miden_crypto/merkle/smt/partial/serialization/
mod.rs

1//! This module contains a system for producing compact serialized representations of the
2//! `PartialSmt` data structure, intended to reduce data sent over the wire through de-duplication.
3
4pub mod property_tests;
5mod tests;
6
7use alloc::{collections::BTreeMap, string::ToString};
8
9use miden_field::Word;
10use miden_serde_utils::{
11    ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
12};
13
14use crate::merkle::{
15    EmptySubtreeRoots, NodeIndex,
16    smt::{LeafIndex, SMT_DEPTH, SmtLeaf},
17};
18
19// UNIQUE NODES
20// ================================================================================================
21
22/// A representation of a partial SMT that contains only the unique nodes in the tree, designed for
23/// better efficiency when sending data across the wire.
24///
25/// It _explicitly_ does not need to contain a fully-realized SMT, and instead may contain some
26/// subset of a full tree. It contains the minimal set of data necessary to reconstruct its input.
27///
28/// # Versioning
29///
30/// Note that this structure is explicitly **not intended to be versioned**. This structure should
31/// be used as part of a broader serialization solution that does include this if necessary.
32///
33/// # Serialization
34///
35/// Deserialization requires nonempty levels with increasing depths and increasing map positions.
36/// It validates node indices, checks embedded leaf indices, and rejects overlap between the leaf
37/// maps. Serializing a value with mismatched leaf indices or overlapping leaf maps panics.
38#[derive(Clone, Debug, Eq, PartialEq)]
39#[cfg_attr(
40    all(feature = "arbitrary", test),
41    miden_test_serialization_macros::serialization_test
42)]
43pub struct UniqueNodes {
44    /// The expected root of the tree after reconstruction.
45    ///
46    /// This primarily exists as a sanity check, taking little extra space but ensuring that we can
47    /// detect more possible cases of corruption.
48    pub root: Word,
49
50    /// The nodes that make up the tree itself.
51    ///
52    /// It maps each node index to its hash. Empty subtree roots are represented by absence.
53    pub nodes: BTreeMap<NodeIndex, Word>,
54
55    /// The leaves of the tree.
56    ///
57    /// It only stores the populated leaves, keyed on their index.
58    pub leaves: BTreeMap<u64, SmtLeaf>,
59
60    /// The leaves for which we only have the hash value, and not the actual leaf value.
61    ///
62    /// We keep these separately to the `leaves` as storing them this way is more compact.
63    pub value_only_leaves: BTreeMap<u64, Word>,
64}
65
66impl UniqueNodes {
67    /// Creates an empty `UniqueNodes` with no nodes or leaves in it.
68    pub fn empty() -> Self {
69        Self {
70            root: *EmptySubtreeRoots::entry(SMT_DEPTH, 0),
71            nodes: BTreeMap::new(),
72            leaves: BTreeMap::new(),
73            value_only_leaves: BTreeMap::new(),
74        }
75    }
76
77    /// Returns the hash of the leaf at `position`, or its canonical empty hash when absent.
78    pub fn get_leaf_hash(&self, position: u64) -> Word {
79        self.leaves
80            .get(&position)
81            .map(SmtLeaf::hash)
82            .or_else(|| self.value_only_leaves.get(&position).copied())
83            .unwrap_or_else(|| SmtLeaf::new_empty(LeafIndex::new_max_depth(position)).hash())
84    }
85
86    /// Returns the hash of the node at `index`, or its canonical empty root when absent.
87    pub fn get_node_hash(&self, index: NodeIndex) -> Word {
88        self.nodes
89            .get(&index)
90            .copied()
91            .unwrap_or_else(|| *EmptySubtreeRoots::entry(SMT_DEPTH, index.depth()))
92    }
93
94    /// Checks embedded leaf positions and exclusivity of the leaf maps.
95    pub(super) fn validate(&self) -> Result<(), DeserializationError> {
96        for (&position, leaf) in &self.leaves {
97            if self.value_only_leaves.contains_key(&position) {
98                return Err(DeserializationError::InvalidValue(format!(
99                    "Leaf position {position} appears in both leaf maps"
100                )));
101            }
102            if position != leaf.index().position() {
103                return Err(DeserializationError::InvalidValue(format!(
104                    "Node index {position} did not match the embedded leaf index {}",
105                    leaf.index().position()
106                )));
107            }
108        }
109
110        Ok(())
111    }
112}
113
114impl Default for UniqueNodes {
115    fn default() -> Self {
116        Self::empty()
117    }
118}
119
120impl Serializable for UniqueNodes {
121    fn write_into<W: ByteWriter>(&self, target: &mut W) {
122        self.validate().expect("cannot serialize invalid UniqueNodes");
123
124        // First we write the expected root into the buffer.
125        self.root.write_into(target);
126
127        // `NodeIndex` sorts first by depth and then by position. Since `nodes` is a `BTreeMap`, all
128        // nodes at the same depth are next to each other and can be written as one level.
129        let mut levels = self.nodes.iter().peekable();
130        let level_count = self
131            .nodes
132            .keys()
133            .map(NodeIndex::depth)
134            .fold((0, None), |(count, previous), depth| {
135                (count + u64::from(previous != Some(depth)), Some(depth))
136            })
137            .0;
138        target.write(level_count);
139
140        while let Some((index, _)) = levels.peek() {
141            let depth = index.depth();
142            target.write(depth);
143            let level_node_count =
144                levels.clone().take_while(|(index, _)| index.depth() == depth).count();
145            target.write(level_node_count as u64);
146            for (index, value) in levels.by_ref().take(level_node_count) {
147                target.write(index.position());
148                target.write(value);
149            }
150        }
151
152        // The leaves themselves come next.
153        let leaf_count = self.leaves.len() as u64;
154        target.write(leaf_count);
155        target.write_many(self.leaves.iter());
156
157        // And the value-only leaves bring up the rear.
158        let value_only_leaf_count = self.value_only_leaves.len() as u64;
159        target.write(value_only_leaf_count);
160        target.write_many(self.value_only_leaves.iter());
161    }
162}
163
164impl Deserializable for UniqueNodes {
165    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
166        // The first item is the 32 bytes containing the expected root of the tree after
167        // reconstruction.
168        let root = Word::read_from(source)?;
169
170        // We first have to read the count of levels.
171        let level_count = source.read_u64()?;
172        let mut nodes = BTreeMap::new();
173        let mut previous_depth = None;
174
175        // Next we have that many levels to read, but each is of a variable size.
176        for _ in 0..level_count {
177            let depth = source.read_u8()?;
178            if let Some(previous) = previous_depth
179                && depth <= previous
180            {
181                return Err(DeserializationError::InvalidValue(format!(
182                    "Level depth {depth} does not exceed previous depth {previous}"
183                )));
184            }
185            previous_depth = Some(depth);
186            let node_count = source.read_u64()?;
187            if node_count == 0 {
188                return Err(DeserializationError::InvalidValue(format!(
189                    "Empty node level at depth {depth}"
190                )));
191            }
192            let mut previous_position = None;
193            for _ in 0..node_count {
194                let position = source.read_u64()?;
195                if let Some(previous) = previous_position
196                    && position < previous
197                {
198                    return Err(DeserializationError::InvalidValue(format!(
199                        "Node position {position} precedes {previous} at depth {depth}"
200                    )));
201                }
202                previous_position = Some(position);
203                let index = NodeIndex::new(depth, position)
204                    .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
205                let value = source.read()?;
206                if nodes.insert(index, value).is_some() {
207                    return Err(DeserializationError::InvalidValue(format!(
208                        "Duplicate node position {position} at depth {depth}"
209                    )));
210                }
211            }
212        }
213
214        // Next we need the number of leaves.
215        let leaf_count = source.read_u64()?;
216        let mut leaves = BTreeMap::new();
217        let mut previous_position = None;
218
219        // And then we have to read that many leaves.
220        for _ in 0..leaf_count {
221            let (position, leaf) = source.read()?;
222            if let Some(previous) = previous_position
223                && position < previous
224            {
225                return Err(DeserializationError::InvalidValue(format!(
226                    "Leaf position {position} precedes {previous}"
227                )));
228            }
229            previous_position = Some(position);
230            if leaves.insert(position, leaf).is_some() {
231                return Err(DeserializationError::InvalidValue(format!(
232                    "Duplicate leaf position {position}"
233                )));
234            }
235        }
236
237        // Finally we read the number of value-only leaves...
238        let value_only_leaf_count = source.read_u64()?;
239        let mut value_only_leaves = BTreeMap::new();
240        let mut previous_position = None;
241
242        // ... and read that many.
243        for _ in 0..value_only_leaf_count {
244            let (position, value) = source.read()?;
245            if let Some(previous) = previous_position
246                && position < previous
247            {
248                return Err(DeserializationError::InvalidValue(format!(
249                    "Value-only leaf position {position} precedes {previous}"
250                )));
251            }
252            previous_position = Some(position);
253            if value_only_leaves.insert(position, value).is_some() {
254                return Err(DeserializationError::InvalidValue(format!(
255                    "Duplicate value-only leaf position {position}"
256                )));
257            }
258        }
259
260        let unique_nodes = Self { root, nodes, leaves, value_only_leaves };
261        unique_nodes.validate()?;
262        Ok(unique_nodes)
263    }
264
265    fn min_serialized_size() -> usize {
266        32 + 3 * 8 // root and three u64 collection counts
267    }
268}
269
270// ARBITRARY (proptest)
271// ================================================================================================
272
273#[cfg(any(test, feature = "arbitrary"))]
274mod unique_nodes_arbitrary {
275    use alloc::collections::BTreeMap;
276
277    use proptest::prelude::*;
278
279    use super::UniqueNodes;
280    use crate::{
281        Felt, Word,
282        merkle::{NodeIndex, smt::Smt},
283    };
284
285    impl Arbitrary for UniqueNodes {
286        type Parameters = ();
287        type Strategy = BoxedStrategy<Self>;
288
289        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
290            // The reader validates each node index against its declared depth
291            // (NodeIndex::new(depth, position)); bound positions to their depth by construction.
292            // BTreeMap deduplication keeps every map key unique; empty maps are valid inputs.
293            (
294                any::<Word>(),
295                proptest::collection::vec((0u8..=64, 0usize..=3), 0..=4),
296                proptest::collection::vec((any::<Word>(), any::<Word>()), 0..=3),
297                proptest::collection::vec(any::<u64>(), 0..=3),
298            )
299                .prop_map(|(root, levels, leaf_pairs, value_only_positions)| {
300                    let mut nodes = BTreeMap::new();
301                    for (depth, count) in levels {
302                        let mask = if depth == 64 { u64::MAX } else { (1u64 << depth) - 1 };
303                        for i in 0..count as u64 {
304                            let position = i.wrapping_mul(0x9e37_79b9_7f4a_7c15) & mask;
305                            // Arbitrary (non-zero-biased) hash: the wire carries node hashes
306                            // verbatim, so the property detects a deserializer that swaps or
307                            // zeroes them.
308                            let hash: Word = [
309                                Felt::from(i as u32 + 1),
310                                Felt::from(depth as u32),
311                                Felt::from(count as u32),
312                                Felt::from(position as u32 % 7),
313                            ]
314                            .into();
315                            nodes.insert(
316                                NodeIndex::new(depth, position).expect("position fits the depth"),
317                                hash,
318                            );
319                        }
320                    }
321
322                    // Leaves are sound by construction: materialize a real Smt and take its
323                    // actual leaves, keyed by their embedded index (the reader cross-validates
324                    // the position against the leaf's embedded index).
325                    let entries: BTreeMap<Word, Word> = leaf_pairs.into_iter().collect();
326                    let smt = Smt::with_entries(entries).expect("keys are distinct");
327                    let leaves: BTreeMap<u64, crate::merkle::smt::SmtLeaf> = smt
328                        .leaves()
329                        .map(|(index, leaf)| (index.position(), leaf.clone()))
330                        .collect();
331
332                    let value_only_leaves: BTreeMap<u64, Word> = value_only_positions
333                        .into_iter()
334                        .filter(|position| !leaves.contains_key(position))
335                        .map(|p| {
336                            let hash: Word = [
337                                Felt::from(p as u32),
338                                Felt::from((p as u32).wrapping_add(1)),
339                                Felt::from((p as u32).wrapping_add(2)),
340                                Felt::from((p as u32).wrapping_add(3)),
341                            ]
342                            .into();
343                            (p, hash)
344                        })
345                        .collect();
346                    Self { root, nodes, leaves, value_only_leaves }
347                })
348                .boxed()
349        }
350    }
351}