didbot_data/mst/shape.rs
1//! The two pure functions a Merkle search tree's *shape* comes from.
2//!
3//! These sit below [`Tree`](super::Tree) and are checked without one, which is
4//! the whole argument: a tree built on a wrong height function is not
5//! merely self-consistent, it is *correct* — every lookup succeeds and every
6//! proof it makes verifies against itself — and the only symptom is a root CID
7//! no other implementation reproduces.
8//!
9//! * [`key_height`] decides which layer a key sits on. It is derived from the
10//! key's own hash, so every implementation puts a key on the same layer
11//! without coordinating.
12//! * [`common_prefix_len`] decides how much of a key a node has to store. A
13//! node's entries are sorted, so each one is written as the number of bytes
14//! it shares with its predecessor plus the rest.
15
16use sha2::{Digest, Sha256};
17
18/// atproto's Merkle search tree has a fanout of four, so two zero bits of the
19/// key's hash buy one layer.
20const FANOUT_BITS: u32 = 2;
21
22/// Which layer of the tree a key belongs on.
23///
24/// The rule is the whole of it: hash the key with sha-256, count the leading
25/// zero *bits* of the digest, and divide by two. Every part of that sentence
26/// is a way to get it wrong — bytes instead of bits gives a height that is
27/// almost always zero, and forgetting the divide doubles every layer — and a
28/// tree built either way still works, which is why this is checked against
29/// `mst/key_heights.json` rather than against a tree.
30///
31/// The key is hashed as its UTF-8 bytes. atproto record keys are ASCII by
32/// rule, so this is not a choice that shows up in the vectors, but it is the
33/// one every implementation makes and it is written down here rather than
34/// left to the caller.
35///
36/// The height is unbounded in principle — a digest of 256 zero bits would
37/// answer 128 — and in practice a key above layer four is already rare. There
38/// is no error case: every string has a hash and every hash has a height.
39///
40/// ```
41/// use didbot_data::mst::key_height;
42///
43/// assert_eq!(key_height("asdf"), 0);
44/// assert_eq!(key_height("blue"), 1);
45/// assert_eq!(key_height("app.bsky.feed.post/9adeb165882c"), 8);
46/// ```
47#[must_use]
48pub fn key_height(key: &str) -> usize {
49 let digest = Sha256::digest(key.as_bytes());
50
51 let mut zeros: u32 = 0;
52 for byte in digest {
53 zeros += byte.leading_zeros();
54 if byte != 0 {
55 break;
56 }
57 }
58
59 (zeros / FANOUT_BITS) as usize
60}
61
62/// How many leading **bytes** two keys share.
63///
64/// This is what lets a node store a suffix rather than a whole key: entries
65/// are sorted, so each one records how much of its predecessor it repeats and
66/// then only the rest. Get it wrong and the node encodes to different bytes,
67/// which means a different CID, which means a different root — the error does
68/// not show up as a lookup that fails, it shows up as a repository nobody else
69/// can verify.
70///
71/// The count is in bytes, not characters, and that is the load-bearing detail.
72/// Two keys sharing a three-character prefix may share four bytes or nine, and
73/// the number a node carries is the one that indexes into the UTF-8. A
74/// character count would agree with a byte count on every ASCII key — which is
75/// every key upstream's vectors contain, and every record key atproto permits
76/// — and would disagree the first time a tree held a key that was neither.
77///
78/// A returned length can therefore land inside a multi-byte character. That is
79/// correct: the tree is keyed on byte strings and the encoding of a node
80/// splits on the byte offset this returns.
81///
82/// ```
83/// use didbot_data::mst::common_prefix_len;
84///
85/// assert_eq!(common_prefix_len("abcde", "abc1"), 3);
86/// assert_eq!(common_prefix_len("abc", "abc\0"), 3);
87/// // Two characters shared, four bytes shared.
88/// assert_eq!(common_prefix_len("ααx", "ααy"), 4);
89/// ```
90#[must_use]
91pub fn common_prefix_len(left: &str, right: &str) -> usize {
92 left.as_bytes()
93 .iter()
94 .zip(right.as_bytes())
95 .take_while(|(left, right)| left == right)
96 .count()
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 /// The empty key has a height like any other: it is the height of the
104 /// sha-256 of nothing, which happens to be zero.
105 #[test]
106 fn the_empty_key_has_a_height() {
107 assert_eq!(key_height(""), 0);
108 }
109
110 /// Counting zero *bytes* rather than zero bits is the mistake this
111 /// guards. `88bfafc7` hashes to a digest beginning `0x0c`: no zero byte at
112 /// all, four leading zero bits, height two.
113 #[test]
114 fn height_counts_bits_not_bytes() {
115 assert_eq!(key_height("88bfafc7"), 2);
116 assert_eq!(
117 Sha256::digest("88bfafc7".as_bytes())[0],
118 0x0c,
119 "the digest this test reasons about has changed"
120 );
121 }
122
123 /// A key whose digest starts with a whole zero byte forces the loop past
124 /// its first iteration, which the low-height keys never do.
125 #[test]
126 fn height_carries_across_a_whole_zero_byte() {
127 // `884976f5` is upstream's height-6 vector: twelve leading zero bits,
128 // so a whole zero byte and then four more.
129 assert_eq!(key_height("884976f5"), 6);
130 assert_eq!(Sha256::digest("884976f5".as_bytes())[0], 0x00);
131 }
132
133 /// Neither argument's length bounds the answer on its own.
134 #[test]
135 fn a_prefix_is_symmetric_and_bounded_by_the_shorter_key() {
136 assert_eq!(common_prefix_len("abcde", "abc"), 3);
137 assert_eq!(common_prefix_len("abc", "abcde"), 3);
138 assert_eq!(common_prefix_len("", "abc"), 0);
139 assert_eq!(common_prefix_len("abc", ""), 0);
140 assert_eq!(common_prefix_len("", ""), 0);
141 }
142
143 /// Bytes, not characters. Upstream's vectors are all ASCII, so this is the
144 /// one claim in this module the interop suite cannot make on its own.
145 #[test]
146 fn a_prefix_is_counted_in_bytes_not_characters() {
147 // Two shared characters, four shared bytes.
148 assert_eq!(common_prefix_len("ααx", "ααy"), 4);
149 // A shared first byte inside characters that differ: 'α' is 0xCE 0xB1
150 // and 'ω' is 0xCF 0x89, so nothing is shared at all here...
151 assert_eq!(common_prefix_len("α", "ω"), 0);
152 // ...while 'α' (0xCE 0xB1) and 'β' (0xCE 0xB2) share their lead byte.
153 assert_eq!(common_prefix_len("α", "β"), 1);
154 }
155}