Skip to main content

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}