penumbra_sdk_tct/storage/
serialize.rs

1//! Incremental serialization for the [`Tree`](crate::Tree).
2
3use poseidon377::Fq;
4use serde::de::Visitor;
5
6use crate::prelude::*;
7
8pub(crate) mod fq;
9
10/// Options for serializing a tree.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12pub(crate) struct Serializer {
13    /// The last position stored in storage, to allow for incremental serialization.
14    last_position: StoredPosition,
15    /// The minimum forgotten version which should be reported for deletion.
16    last_forgotten: Forgotten,
17}
18
19/// Data about an internal hash at a particular point in the tree.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub(crate) struct InternalHash {
22    /// The position of the hash.
23    pub position: Position,
24    /// The height of the hash.
25    pub height: u8,
26    /// The hash.
27    pub hash: Hash,
28    /// Whether the hash is essential to be serialized.
29    ///
30    /// If this is `false`, that means this hash could be omitted and deserialization would be
31    /// correct, but slower.
32    pub essential: bool,
33    /// Whether the children of the node should be deleted.
34    pub delete_children: bool,
35}
36
37impl Serializer {
38    fn is_node_fresh(&self, node: &structure::Node) -> bool {
39        match self.last_position {
40            StoredPosition::Full => false,
41            StoredPosition::Position(last_stored_position) => {
42                let node_position: u64 = node.position().into();
43                let last_stored_position: u64 = last_stored_position.into();
44
45                // If the node is ahead of the last stored position, we need to serialize it
46                node_position >= last_stored_position
47                    || (
48                        // If the height is zero, we don't need to care because the frontier tip is
49                        // always serialized
50                        node.height() > 0
51                        // The harder part: if the node is not ahead of the last stored position, we omitted
52                        // serializing it if it was at that time on the frontier, but we can't skip that now
53                        && self.was_node_on_previous_frontier(node)
54                    )
55            }
56        }
57    }
58
59    fn was_node_on_previous_frontier(&self, node: &structure::Node) -> bool {
60        if let StoredPosition::Position(last_stored_position) = self.last_position {
61            let last_stored_position: u64 = last_stored_position.into();
62
63            if let Some(last_frontier_tip) = last_stored_position.checked_sub(1) {
64                let height = node.height();
65                let node_position: u64 = node.position().into();
66
67                // This is true precisely when the node *was* on the frontier at the time
68                // when the position was `last_stored_position`: because frontier nodes are
69                // not serialized unless they are the leaf, we need to take care of these
70                // also: Shift by height * 2 and compare to compare the leading prefixes of
71                // the position of the hypothetical frontier tip node as of the last stored
72                // position, but only *down to* the height, indicating whether the node we
73                // are examining was on the frontier
74                node_position >> (height * 2) == last_frontier_tip >> (height * 2)
75            } else {
76                false
77            }
78        } else {
79            false
80        }
81    }
82
83    fn node_has_fresh_children(&self, node: &structure::Node) -> bool {
84        self.is_node_fresh(node)
85            || match self.last_position {
86                StoredPosition::Position(last_stored_position) => node
87                    .range()
88                    // Subtract one from the last-stored position to get the frontier tip as of the
89                    // last serialization: if this is in range, some of the node's children might be
90                    // worth investigating
91                    .contains(&u64::from(last_stored_position).saturating_sub(1).into()),
92                StoredPosition::Full => false,
93            }
94    }
95
96    /// Serialize a tree's structure into a depth-first pre-order traversal of hashes within it.
97    pub fn hashes(
98        self,
99        tree: &crate::Tree,
100    ) -> impl Iterator<Item = InternalHash> + Send + Sync + '_ {
101        let mut stack = vec![vec![tree.structure()]];
102
103        std::iter::from_fn(move || {
104            while let Some(level) = stack.last_mut() {
105                if let Some(node) = level.pop() {
106                    let position = node.position();
107                    let height = node.height();
108                    let mut children = node.children();
109                    let has_children = !children.is_empty();
110
111                    // Traverse the children in order, provided that the minimum position doesn't preclude this
112                    if self.node_has_fresh_children(&node) {
113                        children.reverse();
114                        stack.push(children);
115                    }
116
117                    if let Some(hash) = node.cached_hash() {
118                        // A node's hash is recalculable if it has children or if it has a witnessed commitment
119                        let recalculable = has_children
120                            || matches!(
121                                node.kind(),
122                                Kind::Leaf {
123                                    commitment: Some(_)
124                                }
125                            );
126
127                        // A node's hash is essential if it is not recalculable
128                        let essential = !recalculable;
129
130                        // A node is complete if it's not on the frontier
131                        let complete = node.place() == Place::Complete;
132
133                        // A node is fresh if it couldn't have been serialized to storage yet
134                        let fresh = self.is_node_fresh(&node);
135
136                        // We always serialize the frontier leaf hash, even though it's not essential,
137                        // because it's not going to change
138                        let frontier_leaf = !complete && matches!(node.kind(), Kind::Leaf { .. });
139
140                        // We only need to issue an instruction to delete the children if the node
141                        // is both essential and also was previously on the frontier
142                        let delete_children =
143                            essential && self.was_node_on_previous_frontier(&node);
144
145                        // If a node is not default, fresh, and either essential (i.e. the frontier
146                        // leaf) or complete, then we should emit a hash for it
147                        if fresh && (essential || complete || frontier_leaf) {
148                            return Some(InternalHash {
149                                position,
150                                height,
151                                hash,
152                                essential,
153                                delete_children,
154                            });
155                        }
156                    }
157                } else {
158                    stack.pop();
159                }
160            }
161
162            None
163        })
164    }
165
166    /// Serialize a tree's structure into its commitments, in right-to-left order.
167    pub fn commitments(
168        self,
169        tree: &crate::Tree,
170    ) -> impl Iterator<Item = (Position, StateCommitment)> + Send + Sync + '_ {
171        let mut stack = vec![vec![tree.structure()]];
172
173        std::iter::from_fn(move || {
174            while let Some(level) = stack.last_mut() {
175                if let Some(node) = level.pop() {
176                    let position = node.position();
177                    let mut children = node.children();
178
179                    // Traverse the children in order, provided that the minimum position doesn't preclude this
180                    if self.node_has_fresh_children(&node) {
181                        children.reverse();
182                        stack.push(children);
183                    }
184
185                    // If the minimum position is too high, then don't keep this node (but maybe some of
186                    // its children will be kept)
187                    if self.is_node_fresh(&node) {
188                        // If we're at a witnessed commitment, yield it
189                        if let Kind::Leaf {
190                            commitment: Some(commitment),
191                        } = node.kind()
192                        {
193                            return Some((position, commitment));
194                        }
195                    }
196                } else {
197                    stack.pop();
198                }
199            }
200
201            None
202        })
203    }
204
205    /// Get a stream of forgotten locations, which can be deleted from incremental storage.
206    pub fn forgotten(
207        self,
208        tree: &crate::Tree,
209    ) -> impl Iterator<Item = InternalHash> + Send + Sync + '_ {
210        let mut stack = vec![vec![tree.structure()]];
211
212        std::iter::from_fn(move || {
213            while let Some(level) = stack.last_mut() {
214                if let Some(node) = level.pop() {
215                    // Only report nodes (and their children) which are less than the last stored position
216                    // (because those greater will not have yet been serialized to storage) and greater
217                    // than or equal to the minimum forgotten version (because those lesser will already
218                    // have been deleted from storage)
219                    let before_last_stored_position = match self.last_position {
220                        StoredPosition::Full => true,
221                        StoredPosition::Position(last_stored_position) =>
222                        // We don't do anything at all if the node position is greater than or equal
223                        // to the last stored position, because in that case, it, *as well as its
224                        // children* have never been persisted into storage, so no deletions are
225                        // necessary to deal with any things that have been forgotten within them
226                        {
227                            node.position() < last_stored_position
228                        }
229                    };
230
231                    if before_last_stored_position && node.forgotten() > self.last_forgotten {
232                        let mut children = node.children();
233                        if children.is_empty() {
234                            // If there are no children, report the point
235                            // A node with no children definitely has a precalculated hash, so this
236                            // is not evaluating any extra hashes
237                            let hash = node.hash();
238                            return Some(InternalHash {
239                                position: node.position(),
240                                height: node.height(),
241                                hash,
242                                // All forgotten nodes are essential, because they have nothing
243                                // beneath them to witness them
244                                essential: true,
245                                // All forgotten nodes should cause their children to be deleted
246                                delete_children: true,
247                            });
248                        } else {
249                            // If there are children, this node was not yet forgotten, but because the
250                            // node's forgotten version is greater than the minimum forgotten specified
251                            // in the options, we know there is some child which needs to be accounted for
252                            children.reverse();
253                            stack.push(children);
254                        }
255                    }
256                } else {
257                    stack.pop();
258                }
259            }
260
261            None
262        })
263    }
264}
265
266/// Serialize the changes to a [`Tree`](crate::Tree) into an asynchronous writer, deleting all
267/// forgotten nodes and adding all new nodes.
268pub async fn to_async_writer<W: AsyncWrite>(
269    writer: &mut W,
270    tree: &crate::Tree,
271) -> Result<(), W::Error> {
272    // Grab the current position stored in storage
273    let last_position = writer.position().await?;
274
275    // Grab the last forgotten version stored in storage
276    let last_forgotten = writer.forgotten().await?;
277
278    for update in updates(last_position, last_forgotten, tree) {
279        match update {
280            Update::SetPosition(position) => writer.set_position(position).await?,
281            Update::SetForgotten(forgotten) => writer.set_forgotten(forgotten).await?,
282            Update::StoreHash(StoreHash {
283                position,
284                height,
285                hash,
286                essential,
287            }) => {
288                writer.add_hash(position, height, hash, essential).await?;
289            }
290            Update::StoreCommitment(StoreCommitment {
291                position,
292                commitment,
293            }) => {
294                writer.add_commitment(position, commitment).await?;
295            }
296            Update::DeleteRange(DeleteRange {
297                below_height,
298                positions,
299            }) => {
300                writer.delete_range(below_height, positions).await?;
301            }
302        }
303    }
304
305    Ok(())
306}
307
308/// Serialize the changes to a [`Tree`](crate::Tree) into a synchronous writer, deleting all
309/// forgotten nodes and adding all new nodes.
310pub fn to_writer<W: Write>(writer: &mut W, tree: &crate::Tree) -> Result<(), W::Error> {
311    // Grab the current position stored in storage
312    let last_position = writer.position()?;
313
314    // Grab the last forgotten version stored in storage
315    let last_forgotten = writer.forgotten()?;
316
317    for update in updates(last_position, last_forgotten, tree) {
318        match update {
319            Update::SetPosition(position) => writer.set_position(position)?,
320            Update::SetForgotten(forgotten) => writer.set_forgotten(forgotten)?,
321            Update::StoreHash(StoreHash {
322                position,
323                height,
324                hash,
325                essential,
326            }) => {
327                writer.add_hash(position, height, hash, essential)?;
328            }
329            Update::StoreCommitment(StoreCommitment {
330                position,
331                commitment,
332            }) => {
333                writer.add_commitment(position, commitment)?;
334            }
335            Update::DeleteRange(DeleteRange {
336                below_height,
337                positions,
338            }) => {
339                writer.delete_range(below_height, positions)?;
340            }
341        }
342    }
343
344    Ok(())
345}
346
347/// Create an iterator of all the updates to the tree since the specified last position and last
348/// forgotten version.
349pub fn updates(
350    last_position: impl Into<StoredPosition>,
351    last_forgotten: Forgotten,
352    tree: &crate::Tree,
353) -> impl Iterator<Item = storage::Update> + Send + Sync + '_ {
354    if tree.is_empty() {
355        None
356    } else {
357        let last_position = last_position.into();
358
359        let serializer = Serializer {
360            last_forgotten,
361            last_position,
362        };
363
364        let position_updates = Some(if let Some(position) = tree.position() {
365            StoredPosition::Position(position)
366        } else {
367            StoredPosition::Full
368        })
369        .into_iter()
370        .filter(move |&position| position != last_position)
371        .map(storage::Update::SetPosition);
372
373        let forgotten_updates = Some(tree.forgotten())
374            .into_iter()
375            .filter(move |&forgotten| forgotten != last_forgotten)
376            .map(storage::Update::SetForgotten);
377
378        let commitment_updates = serializer.commitments(tree).map(|(position, commitment)| {
379            storage::Update::StoreCommitment(storage::StoreCommitment {
380                position,
381                commitment,
382            })
383        });
384
385        let hash_and_deletion_updates = serializer
386            .forgotten(tree)
387            .chain(serializer.hashes(tree))
388            .flat_map(
389                move |InternalHash {
390                          position,
391                          height,
392                          hash,
393                          essential,
394                          delete_children,
395                      }| {
396                    let deletion_update = if delete_children {
397                        // Calculate the range of positions to delete, based on the height
398                        let position = u64::from(position);
399                        let stride = 4u64.pow(height.into());
400                        let positions =
401                            position.into()..(position + stride).min(4u64.pow(24) - 1).into();
402
403                        // Delete the range of positions
404                        Some(storage::Update::DeleteRange(storage::DeleteRange {
405                            below_height: height,
406                            positions,
407                        }))
408                    } else {
409                        None
410                    };
411
412                    let hash_update = if hash != Hash::one() {
413                        // Optimization: don't serialize `Hash::one()`, because it will be filled in automatically
414                        Some(storage::Update::StoreHash(storage::StoreHash {
415                            position,
416                            height,
417                            hash,
418                            essential,
419                        }))
420                    } else {
421                        None
422                    };
423
424                    // Deleting children, then adding the hash allows the backend to do a sensibility check that
425                    // there are no children of essential hashes, if it chooses to.
426
427                    deletion_update.into_iter().chain(hash_update)
428                },
429            );
430
431        Some(
432            position_updates
433                .chain(forgotten_updates)
434                .chain(commitment_updates)
435                .chain(hash_and_deletion_updates),
436        )
437    }
438    .into_iter()
439    .flatten()
440}