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}