didbot_data/mst/store.rs
1//! A tree somebody else built: nodes by CID, and the walks over them.
2//!
3//! [`Tree`](super::Tree) is this crate's own tree — built from a whole leaf
4//! set, immutable, holding its own blocks. A [`Store`] is the opposite
5//! situation: a pile of blocks that arrived from elsewhere, one of which is
6//! claimed to be a root. Nothing about it is trusted, so every walk here can
7//! fail — a link may name a block that is not in the pile, and a subtree may
8//! claim a layer it is not on.
9//!
10//! Three things are built on the walks:
11//!
12//! * [`Store::leaves`] — what the tree says the repository holds.
13//! * [`Store::find`] — the walk to one path, which is an inclusion proof when
14//! the path is there and an exclusion proof when it is not.
15//! * [`diff`] — what changed between two roots, as the nodes that came and
16//! went, the records that came and went, and the blocks that prove it.
17//!
18//! # Why the walk stops early
19//!
20//! [`Store::find`] gives up as soon as it is below the layer the path's own
21//! height puts it on. A path can only ever be an entry at that one layer, so
22//! a walk that continued would be reading blocks that cannot hold the answer
23//! — and an exclusion proof made of them would be larger than it needs to be
24//! while proving exactly the same thing. This is the rule that decides how
25//! big `proof_nodes` is in the Merkle search tree conformance vectors, and
26//! getting it wrong is not a wrong answer, only a wasteful one.
27
28use std::collections::{BTreeMap, BTreeSet};
29
30use super::read::NodeError;
31use super::{key_height, Node};
32use crate::cid::Cid;
33
34/// Why a tree could not be walked.
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum StoreError {
37 /// A link naming a block the store does not hold.
38 #[error("the tree links to {0}, which is not in the store")]
39 Missing(Box<Cid>),
40 /// A subtree that is not one layer below the node pointing at it.
41 #[error("{cid} says it is at layer {found}, and its parent puts it at {expected}")]
42 Layer {
43 /// The subtree that disagreed.
44 cid: Box<Cid>,
45 /// The layer the node's own keys put it on.
46 found: usize,
47 /// The layer its parent puts it on.
48 expected: usize,
49 },
50 /// A root whose own layer cannot be told, which every walk needs to know.
51 ///
52 /// A node with no keys and a subtree below it is one layer under whatever
53 /// pointed at it, and a root has nothing pointing at it. The tree that
54 /// produced it is malformed: [`Tree::build`](super::Tree::build) strips
55 /// those from the top, and so does every other implementation.
56 #[error("the root {0} has no keys, so nothing says which layer it is on")]
57 RootLayer(Box<Cid>),
58}
59
60/// Blocks that are claimed to be a Merkle search tree.
61///
62/// Nodes are held decoded, because every operation reads them and decoding is
63/// where the structural checks live. The blocks are held too: a proof is
64/// answered as a set of CIDs, and whatever sends it needs the bytes.
65#[derive(Debug, Clone, Default)]
66pub struct Store {
67 /// Every node, by the CID it was filed under.
68 nodes: BTreeMap<Cid, Node>,
69 /// The blocks those nodes were decoded from.
70 blocks: BTreeMap<Cid, Vec<u8>>,
71}
72
73/// What a walk to one path found, and what proves it.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Found {
76 /// The record the path names, if the tree holds it.
77 pub value: Option<Cid>,
78 /// The nodes walked, which are the proof of either answer.
79 ///
80 /// A set rather than a list: it is combined with other proofs far more
81 /// often than it is followed, and the order is recoverable from the
82 /// blocks themselves.
83 pub proof: BTreeSet<Cid>,
84}
85
86impl Store {
87 /// Builds a store over blocks that are all claimed to be nodes.
88 ///
89 /// Every block is decoded here rather than lazily, so a malformed node is
90 /// a failure to load a tree rather than a failure part-way through
91 /// answering a query about it. A CAR file that also holds records is not
92 /// what this takes; see [`Store::insert`] for adding them one at a time.
93 pub fn load(
94 blocks: impl IntoIterator<Item = (Cid, Vec<u8>)>,
95 ) -> Result<Self, (Box<Cid>, NodeError)> {
96 let mut store = Self::default();
97 for (cid, block) in blocks {
98 store
99 .insert(&cid, block)
100 .map_err(|err| (Box::new(cid.clone()), err))?;
101 }
102 Ok(store)
103 }
104
105 /// Decodes one block and files it under `cid`.
106 ///
107 /// The CID is taken on trust: whether the block hashes to it is a
108 /// question about a *container* — see `didbot_repo::car::read`,
109 /// which checks it — and re-hashing here would make every store pay for
110 /// it twice.
111 pub fn insert(&mut self, cid: &Cid, block: Vec<u8>) -> Result<(), NodeError> {
112 let node = Node::decode(&block)?;
113 self.nodes.insert(cid.clone(), node);
114 self.blocks.insert(cid.clone(), block);
115 Ok(())
116 }
117
118 /// Files a node that was built rather than decoded.
119 ///
120 /// Used by anything that *writes* a tree into a store — a rewrite, a
121 /// proof being replayed — where the node is already in hand and encoding
122 /// it only to decode it again would prove nothing.
123 pub fn put(&mut self, node: Node) -> Cid {
124 let (cid, block) = node.block();
125 self.blocks.insert(cid.clone(), block);
126 self.nodes.insert(cid.clone(), node);
127 cid
128 }
129
130 /// The node under `cid`, or the error naming what is missing.
131 pub fn node(&self, cid: &Cid) -> Result<&Node, StoreError> {
132 self.nodes
133 .get(cid)
134 .ok_or_else(|| StoreError::Missing(Box::new(cid.clone())))
135 }
136
137 /// The block under `cid`, if the store holds it.
138 #[must_use]
139 pub fn block(&self, cid: &Cid) -> Option<&[u8]> {
140 self.blocks.get(cid).map(Vec::as_slice)
141 }
142
143 /// How many nodes the store holds.
144 #[must_use]
145 pub fn len(&self) -> usize {
146 self.nodes.len()
147 }
148
149 /// Whether the store holds nothing at all.
150 #[must_use]
151 pub fn is_empty(&self) -> bool {
152 self.nodes.is_empty()
153 }
154
155 /// Which layer a root is on, which is the one thing a walk needs before
156 /// it starts.
157 pub fn root_layer(&self, root: &Cid) -> Result<usize, StoreError> {
158 self.node(root)?
159 .layer()
160 .ok_or_else(|| StoreError::RootLayer(Box::new(root.clone())))
161 }
162
163 /// Every node reachable from `root`, itself included.
164 ///
165 /// This is what a tree *is*, as a set of blocks, and the two directions
166 /// of [`diff`] are the two differences of these sets. Subtree layers are
167 /// checked on the way down: a node claiming the wrong one is a tree that
168 /// two implementations would walk differently.
169 pub fn nodes_under(&self, root: &Cid) -> Result<BTreeSet<Cid>, StoreError> {
170 let mut found = BTreeSet::new();
171 let mut pending = vec![(root.clone(), self.root_layer(root)?)];
172 while let Some((cid, layer)) = pending.pop() {
173 if !found.insert(cid.clone()) {
174 continue;
175 }
176 let node = self.node(&cid)?;
177 self.check_layer(&cid, node, layer)?;
178 for subtree in node.parts().subtrees.into_iter().flatten() {
179 pending.push((subtree, layer.saturating_sub(1)));
180 }
181 }
182 Ok(found)
183 }
184
185 /// Every path in the tree and the record it names, in key order.
186 pub fn leaves(&self, root: &Cid) -> Result<BTreeMap<String, Cid>, StoreError> {
187 let mut leaves = BTreeMap::new();
188 self.collect(root, self.root_layer(root)?, &mut leaves)?;
189 Ok(leaves)
190 }
191
192 /// Appends every path under one node.
193 ///
194 /// The descent is an explicit stack, as [`Store::nodes_under`]'s is, and
195 /// for the same reason: the depth of a tree that arrived from elsewhere
196 /// is whatever its blocks say. A chain of empty nodes is a few bytes per
197 /// link and is walked clean, so recursing once per link would let a small
198 /// pile of blocks end the process.
199 fn collect(
200 &self,
201 cid: &Cid,
202 layer: usize,
203 leaves: &mut BTreeMap<String, Cid>,
204 ) -> Result<(), StoreError> {
205 let mut pending = vec![(cid.clone(), layer)];
206 while let Some((cid, layer)) = pending.pop() {
207 let node = self.node(&cid)?;
208 self.check_layer(&cid, node, layer)?;
209 let parts = node.parts();
210 for (key, value) in parts.keys.into_iter().zip(parts.values) {
211 leaves.insert(key, value);
212 }
213 // Pushed right to left so they pop left to right, which is the
214 // order the keys are in.
215 let below = layer.saturating_sub(1);
216 for subtree in parts.subtrees.into_iter().rev().flatten() {
217 pending.push((subtree, below));
218 }
219 }
220 Ok(())
221 }
222
223 /// Refuses a subtree that is not on the layer its parent puts it on.
224 ///
225 /// An empty intermediate has no layer of its own and is skipped: it
226 /// exists precisely to carry a pointer down a layer nothing sits on.
227 fn check_layer(&self, cid: &Cid, node: &Node, expected: usize) -> Result<(), StoreError> {
228 match node.layer() {
229 Some(found) if found != expected && !node.is_empty() => Err(StoreError::Layer {
230 cid: Box::new(cid.clone()),
231 found,
232 expected,
233 }),
234 _ => Ok(()),
235 }
236 }
237
238 /// Walks to one path, and hands back the walk as well as the answer.
239 ///
240 /// The walk is the proof: a verifier holding these blocks and the root
241 /// CID can repeat it, and reach the same answer or find that the blocks
242 /// do not hash to what they were named. That is true of a path that is
243 /// *not* in the tree as well — the walk ends where the path would have to
244 /// be, and its absence there is its absence everywhere.
245 ///
246 /// The descent stops as soon as it is below the path's own layer; see the
247 /// module documentation for why that is not merely an optimisation.
248 pub fn find(&self, root: &Cid, path: &str) -> Result<Found, StoreError> {
249 let wanted = key_height(path);
250 let mut proof = BTreeSet::new();
251 let mut cid = root.clone();
252 let mut layer = self.root_layer(root)?;
253 proof.insert(cid.clone());
254 loop {
255 // Below the layer the path belongs on, so the tree does not hold
256 // it however far down the blocks go.
257 if wanted > layer {
258 return Ok(Found { value: None, proof });
259 }
260 let node = self.node(&cid)?;
261 self.check_layer(&cid, node, layer)?;
262 let parts = node.parts();
263 let mut index = 0;
264 while index < parts.keys.len() && parts.keys[index].as_str() < path {
265 index += 1;
266 }
267 if parts.keys.get(index).is_some_and(|key| key == path) {
268 return Ok(Found {
269 value: Some(parts.values[index].clone()),
270 proof,
271 });
272 }
273 let Some(subtree) = parts.subtrees[index].clone() else {
274 return Ok(Found { value: None, proof });
275 };
276 proof.insert(subtree.clone());
277 cid = subtree;
278 layer = layer.saturating_sub(1);
279 }
280 }
281}
282
283/// One record that changed between two trees.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct RecordOp {
286 /// The repository path: `<collection>/<record key>`.
287 pub path: String,
288 /// What the record was before, or `None` if it was created.
289 pub old_value: Option<Cid>,
290 /// What it is after, or `None` if it was deleted.
291 pub new_value: Option<Cid>,
292}
293
294/// What changed between two roots.
295///
296/// The two node sets are the difference of the trees as *blocks*, and they
297/// are not symmetric with the records: a node can be created without any
298/// record changing inside it, because a sibling changed and the node above
299/// both had to be rewritten.
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct Diff {
302 /// Nodes the second tree has and the first does not.
303 pub created_nodes: BTreeSet<Cid>,
304 /// Nodes the first tree has and the second does not.
305 pub deleted_nodes: BTreeSet<Cid>,
306 /// The records that changed, in path order.
307 pub ops: Vec<RecordOp>,
308 /// The blocks that prove those record changes against the second root.
309 ///
310 /// One walk per changed path, over the tree *after* the change: an
311 /// inclusion proof for a record that is now there, an exclusion proof for
312 /// one that is now gone. It is usually the created nodes over again, and
313 /// it is neither a subset nor a superset of them in general — a deletion
314 /// can be proved by a node that was already there, and a node can be
315 /// created that no changed path walks through.
316 pub proof_nodes: BTreeSet<Cid>,
317}
318
319/// Diffs two trees held in one store.
320///
321/// Both roots are walked whole, which is the obvious implementation and not
322/// the clever one. A production diff would descend the two trees together and
323/// skip the subtrees whose CIDs already match; this reads every node of both.
324/// For a repository-sized tree that is the difference between a proof and an
325/// export, and it is the right trade here — the operation is checked against
326/// sixteen thousand vectors, and the version that is obviously correct is the
327/// one worth checking them against.
328pub fn diff(store: &Store, before: &Cid, after: &Cid) -> Result<Diff, StoreError> {
329 let old_nodes = store.nodes_under(before)?;
330 let new_nodes = store.nodes_under(after)?;
331 let old_leaves = store.leaves(before)?;
332 let new_leaves = store.leaves(after)?;
333
334 let mut ops = Vec::new();
335 for (path, old_value) in &old_leaves {
336 match new_leaves.get(path) {
337 Some(new_value) if new_value == old_value => {}
338 new_value => ops.push(RecordOp {
339 path: path.clone(),
340 old_value: Some(old_value.clone()),
341 new_value: new_value.cloned(),
342 }),
343 }
344 }
345 for (path, new_value) in &new_leaves {
346 if !old_leaves.contains_key(path) {
347 ops.push(RecordOp {
348 path: path.clone(),
349 old_value: None,
350 new_value: Some(new_value.clone()),
351 });
352 }
353 }
354 ops.sort_by(|left, right| left.path.cmp(&right.path));
355
356 let mut proof_nodes = BTreeSet::new();
357 for op in &ops {
358 proof_nodes.extend(store.find(after, &op.path)?.proof);
359 }
360
361 Ok(Diff {
362 created_nodes: new_nodes.difference(&old_nodes).cloned().collect(),
363 deleted_nodes: old_nodes.difference(&new_nodes).cloned().collect(),
364 ops,
365 proof_nodes,
366 })
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::mst::{Parts, Tree};
373
374 /// A record CID to hang off a path when the test is about the tree.
375 fn record(seed: &str) -> Cid {
376 Cid::of_dag_cbor(seed.as_bytes())
377 }
378
379 /// Builds a tree over `paths` and loads it into a store.
380 fn tree(paths: &[&str]) -> (Store, Cid) {
381 let leaves: BTreeMap<String, Cid> = paths
382 .iter()
383 .map(|path| ((*path).to_owned(), record(path)))
384 .collect();
385 let built = Tree::build(&leaves);
386 let store = Store::load(
387 built
388 .blocks()
389 .iter()
390 .map(|(cid, block)| (cid.clone(), block.clone())),
391 )
392 .expect("a tree this crate built");
393 (store, built.root().clone())
394 }
395
396 /// A tree that went out as blocks comes back as the same leaves. This is
397 /// the round trip the whole file rests on.
398 #[test]
399 fn a_tree_reads_back_the_leaves_it_was_built_from() {
400 let paths: Vec<String> = (0..250)
401 .map(|i| format!("app.bsky.feed.post/3jt6wh3ck{i:03}"))
402 .collect();
403 let leaves: BTreeMap<String, Cid> = paths
404 .iter()
405 .map(|path| (path.clone(), record(path)))
406 .collect();
407 let built = Tree::build(&leaves);
408 let store = Store::load(
409 built
410 .blocks()
411 .iter()
412 .map(|(cid, block)| (cid.clone(), block.clone())),
413 )
414 .expect("a tree this crate built");
415 assert_eq!(store.leaves(built.root()).unwrap(), leaves);
416 assert_eq!(store.nodes_under(built.root()).unwrap().len(), built.len());
417 }
418
419 /// A path in the tree is found, and the walk that found it is shorter
420 /// than the tree.
421 #[test]
422 fn a_walk_finds_a_path_and_is_smaller_than_the_tree() {
423 let paths: Vec<String> = (0..400)
424 .map(|i| format!("com.example.thing/3lqm{i:04}"))
425 .collect();
426 let borrowed: Vec<&str> = paths.iter().map(String::as_str).collect();
427 let (store, root) = tree(&borrowed);
428 let found = store.find(&root, &paths[137]).unwrap();
429 assert_eq!(found.value, Some(record(&paths[137])));
430 assert!(found.proof.len() * 4 < store.len());
431 }
432
433 /// A path the tree does not hold gets a walk and no value, and the walk
434 /// stops at the layer the path would have to be on rather than running to
435 /// a leaf.
436 #[test]
437 fn an_absent_path_stops_at_its_own_layer() {
438 let paths: Vec<String> = (0..400)
439 .map(|i| format!("com.example.thing/3lqm{i:04}"))
440 .collect();
441 let borrowed: Vec<&str> = paths.iter().map(String::as_str).collect();
442 let (store, root) = tree(&borrowed);
443 let missing = "com.example.thing/zzzzzzzz";
444 let found = store.find(&root, missing).unwrap();
445 assert_eq!(found.value, None);
446 let deepest = found
447 .proof
448 .iter()
449 .map(|cid| store.node(cid).unwrap().layer().unwrap_or(usize::MAX))
450 .min()
451 .expect("the walk visited a node");
452 assert!(
453 deepest >= key_height(missing),
454 "the walk went below the layer the path belongs on"
455 );
456 }
457
458 /// A link the store does not hold is an error naming it, rather than a
459 /// tree that quietly reports fewer records than it has.
460 #[test]
461 fn a_missing_block_is_named() {
462 let (mut store, root) = tree(&["a", "b", "c", "d", "e", "f", "g", "h"]);
463 let under = store.nodes_under(&root).unwrap();
464 let child = under
465 .iter()
466 .find(|cid| *cid != &root)
467 .expect("the tree has more than one node")
468 .clone();
469 store.nodes.remove(&child);
470 store.blocks.remove(&child);
471 assert_eq!(
472 store.leaves(&root),
473 Err(StoreError::Missing(Box::new(child)))
474 );
475 }
476
477 /// One record added: the record shows up as a create, and the nodes that
478 /// had to be rewritten show up as created.
479 #[test]
480 fn adding_one_record_is_one_op() {
481 let (mut store, before) = tree(&["a", "b", "c", "d"]);
482 let (after_store, after) = tree(&["a", "b", "c", "d", "e"]);
483 for (cid, block) in after_store.blocks {
484 store.insert(&cid, block).unwrap();
485 }
486 let diff = diff(&store, &before, &after).unwrap();
487 assert_eq!(
488 diff.ops,
489 vec![RecordOp {
490 path: "e".to_owned(),
491 old_value: None,
492 new_value: Some(record("e")),
493 }]
494 );
495 assert!(!diff.created_nodes.is_empty());
496 assert!(diff.created_nodes.contains(&after));
497 assert!(diff.deleted_nodes.contains(&before));
498 // The proof of a created record is a walk through the new tree, and
499 // every node on it is one the new tree holds.
500 for cid in &diff.proof_nodes {
501 assert!(after_store_holds(&store, &after, cid));
502 }
503 }
504
505 /// Whether a node is in the tree under `root`.
506 fn after_store_holds(store: &Store, root: &Cid, cid: &Cid) -> bool {
507 store.nodes_under(root).unwrap().contains(cid)
508 }
509
510 /// A tree diffed against itself changed nothing, which is the case a
511 /// firehose consumer sees most often and the one an off-by-one in the
512 /// set arithmetic breaks first.
513 #[test]
514 fn a_tree_against_itself_is_empty() {
515 let (store, root) = tree(&["a", "b", "c", "d"]);
516 let diff = diff(&store, &root, &root).unwrap();
517 assert!(diff.created_nodes.is_empty());
518 assert!(diff.deleted_nodes.is_empty());
519 assert!(diff.ops.is_empty());
520 assert!(diff.proof_nodes.is_empty());
521 }
522
523 /// A record whose value changed is an update, with both sides named.
524 #[test]
525 fn a_changed_value_is_an_update() {
526 let mut leaves: BTreeMap<String, Cid> = ["a", "b", "c"]
527 .iter()
528 .map(|path| ((*path).to_owned(), record(path)))
529 .collect();
530 let before_tree = Tree::build(&leaves);
531 leaves.insert("b".to_owned(), record("something else"));
532 let after_tree = Tree::build(&leaves);
533 let mut store = Store::default();
534 for tree in [&before_tree, &after_tree] {
535 for (cid, block) in tree.blocks() {
536 store.insert(cid, block.clone()).unwrap();
537 }
538 }
539 let diff = diff(&store, before_tree.root(), after_tree.root()).unwrap();
540 assert_eq!(
541 diff.ops,
542 vec![RecordOp {
543 path: "b".to_owned(),
544 old_value: Some(record("b")),
545 new_value: Some(record("something else")),
546 }]
547 );
548 }
549
550 /// A deep chain of blocks is walked without ending the process.
551 ///
552 /// An empty intermediate is a handful of bytes and is walked clean —
553 /// [`Store::check_layer`] passes it whatever layer its parent claims —
554 /// so a chain of them is the cheapest deep tree a sender can build. A
555 /// walk that took one stack frame per link would reach the guard page
556 /// here, and a stack overflow is not something the caller of a read can
557 /// catch.
558 #[test]
559 fn a_deep_chain_of_empty_nodes_is_walked_rather_than_overflowing() {
560 const LINKS: usize = 100_000;
561
562 let mut store = Store::default();
563 // The bottom of the chain: an empty node with nothing under it.
564 let mut below = store.put(Node::from_parts(&Parts {
565 keys: Vec::new(),
566 values: Vec::new(),
567 subtrees: vec![None],
568 }));
569 for _ in 0..LINKS {
570 below = store.put(Node::from_parts(&Parts {
571 keys: Vec::new(),
572 values: Vec::new(),
573 subtrees: vec![Some(below)],
574 }));
575 }
576 // A root has to carry a key for its layer to be readable at all, so
577 // the chain hangs off one entry and that entry is the only leaf.
578 let path = "com.example.thing/3lqm0000";
579 let root = store.put(Node::from_parts(&Parts {
580 keys: vec![path.to_owned()],
581 values: vec![record(path)],
582 subtrees: vec![Some(below), None],
583 }));
584
585 assert_eq!(
586 store.leaves(&root).unwrap(),
587 BTreeMap::from([(path.to_owned(), record(path))])
588 );
589 assert_eq!(store.nodes_under(&root).unwrap().len(), LINKS + 2);
590 }
591}