Skip to main content

didbot_data/mst/
tree.rs

1//! The tree: nodes, the layering, and the blocks they encode to.
2
3use std::collections::BTreeMap;
4
5use super::{common_prefix_len, key_height};
6use crate::cid::Cid;
7use crate::dag_cbor;
8use crate::value::Value;
9
10/// One entry in a node: a key, the record it names, and what hangs to its
11/// right.
12///
13/// The key is stored split, because that is how it is encoded. A node's
14/// entries are sorted, so each one records how many leading **bytes** it
15/// shares with its predecessor and then only the rest — `p` and `k` in the
16/// node encoding. The first entry in a node shares nothing, because it has no
17/// predecessor *in that node*; the key of the last entry in the subtree to its
18/// left is not what the prefix is counted against.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Entry {
21    /// How many leading bytes this key shares with the previous entry's.
22    pub prefix: usize,
23    /// The rest of the key, from `prefix` onwards, as bytes.
24    pub suffix: Vec<u8>,
25    /// The record this key names.
26    pub value: Cid,
27    /// The subtree of keys that sort after this one and before the next.
28    pub tree: Option<Cid>,
29}
30
31/// One node of the tree.
32///
33/// A node holds every key at its own layer within some range, and a pointer
34/// to a subtree between each pair of them. `left` is the subtree before the
35/// first entry. A node with no entries is legal and common — it is what the
36/// layering rule produces when a run of low keys hangs under a much taller
37/// one — and it carries `left` alone.
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct Node {
40    /// Keys below this node's layer that sort before every entry.
41    pub left: Option<Cid>,
42    /// This node's own keys, in order.
43    pub entries: Vec<Entry>,
44}
45
46impl Node {
47    /// The node as a data-model value, which is what it is hashed as.
48    ///
49    /// The field names are the protocol's and are one letter each: `l` for the
50    /// left subtree, `e` for the entries, and within an entry `p` for the
51    /// shared prefix length, `k` for the rest of the key, `v` for the record
52    /// and `t` for the subtree to its right. `k` is a **byte string** and not
53    /// text — a node is keyed on bytes, and a prefix length may land inside a
54    /// multi-byte character.
55    ///
56    /// Ordering is not decided here. [`dag_cbor`] sorts map keys length-first
57    /// and then bytewise, so a node built with the fields in any order encodes
58    /// to the same bytes.
59    #[must_use]
60    pub fn to_value(&self) -> Value {
61        let entries = self
62            .entries
63            .iter()
64            .map(|entry| {
65                Value::Map(BTreeMap::from([
66                    ("p".to_owned(), Value::Integer(entry.prefix as i64)),
67                    ("k".to_owned(), Value::Bytes(entry.suffix.clone())),
68                    ("v".to_owned(), Value::Link(entry.value.clone())),
69                    ("t".to_owned(), link_or_null(entry.tree.as_ref())),
70                ]))
71            })
72            .collect();
73        Value::Map(BTreeMap::from([
74            ("l".to_owned(), link_or_null(self.left.as_ref())),
75            ("e".to_owned(), Value::List(entries)),
76        ]))
77    }
78}
79
80/// A link, or the null a missing subtree is written as.
81///
82/// The field is always present. Omitting it would be a different map with a
83/// different encoding and therefore a different CID.
84fn link_or_null(cid: Option<&Cid>) -> Value {
85    cid.map_or(Value::Null, |cid| Value::Link(cid.clone()))
86}
87
88/// A built Merkle search tree: a root CID and every block under it.
89///
90/// Built from a leaf set in one pass, then changed one key at a time by
91/// [`Tree::insert`] and [`Tree::remove`], which leave it exactly the tree
92/// [`Tree::build`] makes from the new leaf set. The blocks are handed
93/// back rather than written anywhere, because what wants them differs — a CAR
94/// export wants all of them, a proof wants the ones on one key's path — and a
95/// tree that also owned a block store would have to be told which.
96#[derive(Debug, Clone)]
97pub struct Tree {
98    pub(super) root: Cid,
99    /// The layer the root sits on: the height of the tallest key, or zero.
100    pub(super) layer: usize,
101    pub(super) blocks: BTreeMap<Cid, Vec<u8>>,
102    /// The same nodes, still as nodes.
103    ///
104    /// Kept alongside the encoded blocks rather than instead of them because
105    /// [`Tree::path`] has to *read* a node to say which way to go, and this
106    /// crate has no DAG-CBOR decoder to read one back out of its block.
107    /// The duplication is bounded by the tree's node count, which is a small
108    /// fraction of its record count.
109    pub(super) nodes: BTreeMap<Cid, Node>,
110}
111
112/// Which blocks prove what a tree says about one key.
113///
114/// The nodes are the walk from the root down, in that order, and each one
115/// names the next: a verifier hashes each block to check it is the node it
116/// was told to expect, reads the entry range it lands in, and follows the
117/// link. `value` is the record's CID when the key is in the tree and `None`
118/// when it is not — and the *absence* is proved by the same nodes, because
119/// the last one on the path is where the key would have to be if it existed.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Path {
122    /// The nodes walked, root first.
123    pub nodes: Vec<Cid>,
124    /// The record the key names, if the tree holds it.
125    pub value: Option<Cid>,
126}
127
128impl Tree {
129    /// Builds the tree over a leaf set.
130    ///
131    /// The keys are the repository's, which atproto writes as
132    /// `<collection>/<rkey>`, and the values are the CIDs of the records they
133    /// name. A `BTreeMap<String, _>` is already in the order the tree needs:
134    /// `str` compares bytewise, and bytewise over the keys is exactly the
135    /// ordering a node's entries are in.
136    ///
137    /// An empty repository is not an absent tree. It is one node holding no
138    /// entries and no left pointer, and it has a CID like any other, which is
139    /// what an account's first commit signs.
140    ///
141    /// ```
142    /// use std::collections::BTreeMap;
143    /// use didbot_data::{mst::Tree, Cid};
144    ///
145    /// let empty = Tree::build(&BTreeMap::new());
146    /// assert_eq!(
147    ///     empty.root().to_string(),
148    ///     "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm"
149    /// );
150    /// assert_eq!(empty.blocks().len(), 1);
151    /// ```
152    #[must_use]
153    pub fn build(leaves: &BTreeMap<String, Cid>) -> Self {
154        let entries: Vec<(&str, &Cid)> = leaves
155            .iter()
156            .map(|(key, value)| (key.as_str(), value))
157            .collect();
158        // The root sits at the height of the tallest key, with nothing above
159        // it. An empty repository has no tallest key and roots at layer zero.
160        let top = entries
161            .iter()
162            .map(|(key, _)| key_height(key))
163            .max()
164            .unwrap_or(0);
165        let mut blocks = BTreeMap::new();
166        let mut nodes = BTreeMap::new();
167        let root = build_layer(&entries, top, &mut blocks, &mut nodes);
168        Self {
169            root,
170            layer: top,
171            blocks,
172            nodes,
173        }
174    }
175
176    /// The path through the tree to `key`, whether or not it is there.
177    ///
178    /// This is what an inclusion proof is made of, and what makes it a proof
179    /// rather than an export: the nodes on one root-to-key walk and nothing
180    /// else. A repository of ten thousand records proves any one of them with
181    /// a handful of blocks, and a verifier that is handed the whole
182    /// repository instead has been given an export with a different name.
183    ///
184    /// A key the tree does not hold gets the same walk with no `value`. That
185    /// is the exclusion proof: the last node on it is the one the key would
186    /// have to be in, and a verifier that reaches it and does not find the key
187    /// has been shown the key is not in the tree at all.
188    #[must_use]
189    pub fn path(&self, key: &str) -> Path {
190        let mut walked = Vec::new();
191        let mut current = self.root.clone();
192        loop {
193            walked.push(current.clone());
194            // Unreachable for a tree this crate built: every link in it names
195            // a node this crate stored. Answering with the walk so far rather
196            // than panicking keeps a corrupted tree a bad proof instead of a
197            // dead server.
198            let Some(node) = self.nodes.get(&current) else {
199                return Path {
200                    nodes: walked,
201                    value: None,
202                };
203            };
204            match step(node, key.as_bytes()) {
205                Step::Found(value) => {
206                    return Path {
207                        nodes: walked,
208                        value: Some(value),
209                    }
210                }
211                Step::Descend(next) => current = next,
212                Step::Absent => {
213                    return Path {
214                        nodes: walked,
215                        value: None,
216                    }
217                }
218            }
219        }
220    }
221
222    /// Every key in the tree, in the order the tree holds them.
223    ///
224    /// Reconstructed from the nodes rather than remembered from the leaf set,
225    /// so it says what the tree *is* rather than what it was built from. A
226    /// node stores each key as a shared prefix length and a suffix, and the
227    /// prefix is counted against the previous entry **in that node**, so the
228    /// walk has to rebuild the keys node by node rather than across the whole
229    /// tree.
230    #[must_use]
231    pub fn keys(&self) -> Vec<String> {
232        let mut found = Vec::new();
233        self.walk_keys(&self.root, &mut found);
234        found
235    }
236
237    /// Appends every key under one node, in order.
238    fn walk_keys(&self, node: &Cid, found: &mut Vec<String>) {
239        // A link this crate did not store is a corrupted tree, and answering
240        // with the keys found so far keeps that a short answer rather than a
241        // panic — the same choice [`Tree::path`] makes.
242        let Some(node) = self.nodes.get(node) else {
243            return;
244        };
245        if let Some(left) = &node.left {
246            self.walk_keys(left, found);
247        }
248        let mut previous: Vec<u8> = Vec::new();
249        for entry in &node.entries {
250            let mut full = previous[..entry.prefix.min(previous.len())].to_vec();
251            full.extend_from_slice(&entry.suffix);
252            // A repository key is a collection and a record key, both of which
253            // are ASCII, so this is lossless in practice. It is written as a
254            // lossy conversion rather than an unwrap because a node is bytes
255            // and nothing in the encoding promises otherwise.
256            found.push(String::from_utf8_lossy(&full).into_owned());
257            if let Some(tree) = &entry.tree {
258                self.walk_keys(tree, found);
259            }
260            previous = full;
261        }
262    }
263
264    /// The blocks a consumer needs to verify one key changing.
265    ///
266    /// atproto calls this a *covering proof*, and it is what a firehose commit
267    /// carries so that a subscriber can check an operation without holding the
268    /// repository. It is more than [`Tree::path`]: adding or removing a key
269    /// can split a node or merge two, so the nodes that move are the ones on
270    /// the key's own walk **and** on the walks to the keys either side of it.
271    /// A proof of the key alone verifies the lookup and cannot reproduce the
272    /// root the operation started from.
273    ///
274    /// The neighbours are found by listing the keys, which is a walk of the
275    /// whole tree. A caller that already keeps the keys in order finds them
276    /// itself and calls [`Tree::covering_proof_between`].
277    #[must_use]
278    pub fn covering_proof(&self, key: &str) -> BTreeMap<Cid, Vec<u8>> {
279        let keys = self.keys();
280        let before = keys.iter().rev().find(|held| held.as_str() < key);
281        let after = keys.iter().find(|held| held.as_str() > key);
282        self.covering_proof_between(key, before.map(String::as_str), after.map(String::as_str))
283    }
284
285    /// [`Tree::covering_proof`], given the keys either side of `key`.
286    ///
287    /// `before` and `after` are the nearest keys the tree holds below and
288    /// above `key`, or `None` where there is none.
289    #[must_use]
290    pub fn covering_proof_between(
291        &self,
292        key: &str,
293        before: Option<&str>,
294        after: Option<&str>,
295    ) -> BTreeMap<Cid, Vec<u8>> {
296        let mut proof = BTreeMap::new();
297        for walked in std::iter::once(key).chain(before).chain(after) {
298            for node in self.path(walked).nodes {
299                if let Some(block) = self.blocks.get(&node) {
300                    proof.insert(node, block.clone());
301                }
302            }
303        }
304        proof
305    }
306
307    /// The CID a commit signs over.
308    #[must_use]
309    pub fn root(&self) -> &Cid {
310        &self.root
311    }
312
313    /// Every node block in the tree, keyed by its CID.
314    #[must_use]
315    pub fn blocks(&self) -> &BTreeMap<Cid, Vec<u8>> {
316        &self.blocks
317    }
318
319    /// The nodes, still as nodes, keyed by CID.
320    #[must_use]
321    pub fn nodes(&self) -> &BTreeMap<Cid, Node> {
322        &self.nodes
323    }
324
325    /// How many nodes the tree has.
326    #[must_use]
327    pub fn len(&self) -> usize {
328        self.blocks.len()
329    }
330
331    /// Never true: a tree always has at least its root node.
332    ///
333    /// Present because `len` without `is_empty` is a lint, and answering it
334    /// honestly is better than suppressing the lint — an empty repository
335    /// still has a root, which is the whole reason a first commit can be
336    /// signed.
337    #[must_use]
338    pub fn is_empty(&self) -> bool {
339        false
340    }
341}
342
343/// Builds one node at `layer` and everything under it.
344///
345/// Every entry handed in has a height at or below `layer`, and the caller has
346/// established that this layer is one somebody asked for. Keys at the layer
347/// become entries; runs of lower keys between them become subtrees, each built
348/// at exactly one layer down whether or not a key sits there. That last clause
349/// is the load-bearing one — see the module documentation.
350fn build_layer(
351    entries: &[(&str, &Cid)],
352    layer: usize,
353    blocks: &mut BTreeMap<Cid, Vec<u8>>,
354    nodes: &mut BTreeMap<Cid, Node>,
355) -> Cid {
356    let mut node = Node::default();
357    let mut below: Vec<(&str, &Cid)> = Vec::new();
358    let mut previous: Option<&str> = None;
359
360    for (key, value) in entries {
361        if key_height(key) == layer {
362            attach(&mut node, &mut below, layer, blocks, nodes);
363            let prefix = previous.map_or(0, |previous| common_prefix_len(previous, key));
364            node.entries.push(Entry {
365                prefix,
366                suffix: key.as_bytes()[prefix..].to_vec(),
367                value: (*value).clone(),
368                tree: None,
369            });
370            previous = Some(key);
371        } else {
372            below.push((key, value));
373        }
374    }
375    attach(&mut node, &mut below, layer, blocks, nodes);
376
377    store(node, blocks, nodes)
378}
379
380/// Where one node says to go next for a key.
381enum Step {
382    /// The key is an entry in this node, naming this record.
383    Found(Cid),
384    /// The key, if it exists, is in this subtree.
385    Descend(Cid),
386    /// The key would be in this node and is not, and there is no subtree
387    /// under it to look in. The walk ends and the key is not in the tree.
388    Absent,
389}
390
391/// Reads one node and says which way `key` lies.
392///
393/// A node's entries are stored with their keys split against the previous
394/// one, so the full keys are rebuilt as the entries are walked — the same
395/// reconstruction any other implementation does on the way down.
396fn step(node: &Node, key: &[u8]) -> Step {
397    let mut previous: Vec<u8> = Vec::new();
398    // The subtree to the left of the entry being considered. It starts as the
399    // node's own left pointer and becomes each entry's right-hand subtree as
400    // the walk passes it, which is the same statement as "a subtree sits
401    // between the entry before it and the entry after it".
402    let mut before = node.left.clone();
403    for entry in &node.entries {
404        let mut full = previous[..entry.prefix.min(previous.len())].to_vec();
405        full.extend_from_slice(&entry.suffix);
406        match key.cmp(full.as_slice()) {
407            std::cmp::Ordering::Less => {
408                return before.map_or(Step::Absent, Step::Descend);
409            }
410            std::cmp::Ordering::Equal => return Step::Found(entry.value.clone()),
411            std::cmp::Ordering::Greater => {
412                before = entry.tree.clone();
413                previous = full;
414            }
415        }
416    }
417    before.map_or(Step::Absent, Step::Descend)
418}
419
420/// Builds the pending run of lower keys and hangs it off the node.
421///
422/// It goes on the last entry if there is one and on `left` if there is not,
423/// which is the same statement as "the subtree sits between the entry before
424/// it and the entry after it".
425fn attach<'a>(
426    node: &mut Node,
427    below: &mut Vec<(&'a str, &'a Cid)>,
428    layer: usize,
429    blocks: &mut BTreeMap<Cid, Vec<u8>>,
430    nodes: &mut BTreeMap<Cid, Node>,
431) {
432    if below.is_empty() {
433        return;
434    }
435    // Unreachable: at layer zero every key has height zero, so nothing is
436    // ever below it. Written as a saturating step rather than a subtraction
437    // so that a future caller's mistake is a wrong tree and not a panic in a
438    // server.
439    debug_assert!(layer > 0, "a key cannot sit below layer zero");
440    let subtree = build_layer(below, layer.saturating_sub(1), blocks, nodes);
441    match node.entries.last_mut() {
442        Some(last) => last.tree = Some(subtree),
443        None => node.left = Some(subtree),
444    }
445    below.clear();
446}
447
448/// Encodes a node, records its block and the node itself, and answers its CID.
449fn store(node: Node, blocks: &mut BTreeMap<Cid, Vec<u8>>, nodes: &mut BTreeMap<Cid, Node>) -> Cid {
450    let block = dag_cbor::encode(&node.to_value());
451    let cid = Cid::of_dag_cbor(&block);
452    blocks.insert(cid.clone(), block);
453    nodes.insert(cid.clone(), node);
454    cid
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    /// A CID to hang off a key when the test is about the tree and not about
462    /// what the leaves hold.
463    fn leaf(seed: &str) -> Cid {
464        Cid::of_dag_cbor(seed.as_bytes())
465    }
466
467    fn tree_of(keys: &[&str]) -> Tree {
468        let leaves: BTreeMap<String, Cid> = keys
469            .iter()
470            .map(|key| ((*key).to_owned(), leaf(key)))
471            .collect();
472        Tree::build(&leaves)
473    }
474
475    /// The shape is a function of the key set alone, so the order the leaves
476    /// were collected in cannot reach it. This is the property that makes two
477    /// servers agree, and it is worth asserting even though a `BTreeMap`
478    /// makes it hard to violate.
479    #[test]
480    fn the_root_does_not_depend_on_insertion_order() {
481        let forwards = tree_of(&["a", "b", "c", "d", "e", "f", "g", "h"]);
482        let backwards = tree_of(&["h", "g", "f", "e", "d", "c", "b", "a"]);
483        assert_eq!(forwards.root(), backwards.root());
484        assert_eq!(forwards.blocks(), backwards.blocks());
485    }
486
487    /// One record's tree is one node holding one entry, and it is not the
488    /// empty tree.
489    #[test]
490    fn a_single_record_roots_at_its_own_node() {
491        let single = tree_of(&["app.bsky.feed.post/asdf"]);
492        let empty = Tree::build(&BTreeMap::new());
493        assert_eq!(single.len(), 1);
494        assert_ne!(single.root(), empty.root());
495    }
496
497    /// Changing one leaf changes the root. Trivially true of a Merkle
498    /// structure and the reason one is used, so it is stated.
499    #[test]
500    fn a_changed_leaf_moves_the_root() {
501        let mut leaves: BTreeMap<String, Cid> = ["a", "b", "c"]
502            .iter()
503            .map(|k| ((*k).to_owned(), leaf(k)))
504            .collect();
505        let before = Tree::build(&leaves).root().clone();
506        leaves.insert("b".to_owned(), leaf("something else"));
507        assert_ne!(Tree::build(&leaves).root(), &before);
508    }
509
510    /// The first entry of a node shares no prefix, because it has no
511    /// predecessor in that node — even when the key immediately before it in
512    /// the repository shares most of its bytes.
513    #[test]
514    fn a_nodes_first_entry_carries_its_whole_key() {
515        // Both at layer 0, so both land in one node.
516        let tree = tree_of(&["app.bsky.feed.post/aaaa", "app.bsky.feed.post/aaab"]);
517        let block = tree.blocks().get(tree.root()).expect("the root block");
518        // The whole first key appears in the block; the second is stored as
519        // its last byte alone.
520        let needle = b"app.bsky.feed.post/aaaa";
521        assert!(
522            block.windows(needle.len()).any(|window| window == needle),
523            "the first entry should carry its whole key"
524        );
525    }
526
527    /// Every block a tree hands back is the encoding of the CID it is filed
528    /// under. A block store that disagreed with its own keys would produce a
529    /// CAR nothing could walk.
530    #[test]
531    fn every_block_hashes_to_the_cid_it_is_filed_under() {
532        let tree = tree_of(&[
533            "app.bsky.feed.post/3jt6wh3ckcc2y",
534            "app.bsky.feed.like/3jtdufrp6md2l",
535            "com.example.thing/3lqmnopqrstuv",
536            "app.bsky.actor.profile/self",
537        ]);
538        for (cid, block) in tree.blocks() {
539            assert_eq!(&Cid::of_dag_cbor(block), cid);
540        }
541    }
542
543    /// The path to a key is the walk that reaches it, and the last node on it
544    /// holds the record's CID.
545    #[test]
546    fn a_path_ends_at_the_node_holding_the_key() {
547        let tree = tree_of(&["a", "b", "c", "d", "e", "f", "g", "h"]);
548        let path = tree.path("d");
549        assert_eq!(path.value.as_ref(), Some(&leaf("d")));
550        assert_eq!(path.nodes.first(), Some(tree.root()));
551        // Every node on the walk is one the tree actually holds.
552        for cid in &path.nodes {
553            assert!(tree.blocks().contains_key(cid), "{cid} is not in the tree");
554        }
555    }
556
557    /// A key the tree does not hold gets the same walk and no value. That is
558    /// the exclusion proof, and it is why `path` does not return an `Option`.
559    #[test]
560    fn a_missing_key_still_has_a_path() {
561        let tree = tree_of(&["a", "b", "c", "d"]);
562        let path = tree.path("zzzz");
563        assert_eq!(path.value, None);
564        assert!(!path.nodes.is_empty());
565        assert_eq!(path.nodes.first(), Some(tree.root()));
566    }
567
568    /// The point of a proof: it is the path and not the repository. A tree
569    /// with hundreds of records proves one of them with a handful of blocks.
570    #[test]
571    fn a_path_is_shorter_than_the_tree() {
572        let keys: Vec<String> = (0..400)
573            .map(|i| format!("com.example.thing/3lqm{i:04}"))
574            .collect();
575        let leaves: BTreeMap<String, Cid> =
576            keys.iter().map(|key| (key.clone(), leaf(key))).collect();
577        let tree = Tree::build(&leaves);
578        let path = tree.path(&keys[137]);
579        assert_eq!(path.value.as_ref(), Some(&leaf(&keys[137])));
580        assert!(
581            path.nodes.len() * 4 < tree.len(),
582            "a path of {} nodes out of {} is not a proof",
583            path.nodes.len(),
584            tree.len()
585        );
586    }
587
588    /// Every key in the tree is reachable by a walk, which is the property a
589    /// wrong `step` would break for exactly the keys that sit in a subtree.
590    #[test]
591    fn every_key_is_reachable_by_its_own_path() {
592        let keys: Vec<String> = (0..200)
593            .map(|i| format!("app.bsky.feed.post/3jt6wh3ck{i:03}"))
594            .collect();
595        let leaves: BTreeMap<String, Cid> =
596            keys.iter().map(|key| (key.clone(), leaf(key))).collect();
597        let tree = Tree::build(&leaves);
598        for key in &keys {
599            assert_eq!(
600                tree.path(key).value.as_ref(),
601                Some(&leaf(key)),
602                "{key} is in the tree and its path did not find it"
603            );
604        }
605    }
606
607    /// A prefix length may land inside a multi-byte character, because the
608    /// tree is keyed on bytes. Not a key atproto permits, but the encoding
609    /// has to be right for the same reason `common_prefix_len` counts bytes.
610    #[test]
611    fn a_prefix_may_split_a_character() {
612        let tree = tree_of(&["ααx", "ααy"]);
613        for (cid, block) in tree.blocks() {
614            assert_eq!(&Cid::of_dag_cbor(block), cid);
615        }
616    }
617}