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(¤t) 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}