Skip to main content

rfc9162_tiles/
tree.rs

1//! Hash tiles: how the writer grows them, how a reader checks them, and the
2//! root both agree on.
3//!
4//! The tree over `size` leaves is a right fold over its perfect subtrees,
5//! largest first (RFC 9162 splits at the largest power of two below the
6//! width). Those subtree roots are exactly the hashes of the partial tiles,
7//! so the frontier of a log is its partial tiles and nothing else, and a
8//! reader checks every partial tile at once by recomputing the root from
9//! them. A full tile is checked against the hash it folds to in the tile one
10//! level up.
11
12use std::cell::{Cell, RefCell};
13use std::collections::HashMap;
14use std::rc::Rc;
15
16use crate::error::{Error, Result};
17use crate::layout::{Layout, Tile, HASH_SIZE, TILE_HEIGHT, TILE_WIDTH};
18use crate::store::TileStore;
19
20/// A hash as the tree stores it.
21pub type Hash = [u8; HASH_SIZE];
22
23/// Checked tiles by (level, index).
24type TileCache = RefCell<HashMap<(u8, u64), Rc<Vec<Hash>>>>;
25
26/// The bytes of a tile: its hashes, concatenated.
27#[must_use]
28pub fn encode_tile(hashes: &[Hash]) -> Vec<u8> {
29    let mut out = Vec::with_capacity(hashes.len() * HASH_SIZE);
30    for hash in hashes {
31        out.extend_from_slice(hash);
32    }
33    out
34}
35
36/// The hashes of a tile's bytes; refused unless 1 to 256 whole hashes.
37pub fn decode_tile(path: &str, bytes: &[u8]) -> Result<Vec<Hash>> {
38    if bytes.is_empty() || bytes.len() % HASH_SIZE != 0 || bytes.len() > 256 * HASH_SIZE {
39        return Err(Error::Corrupt {
40            path: path.to_string(),
41            reason: format!("{} bytes is not 1 to 256 hashes", bytes.len()),
42        });
43    }
44    Ok(bytes
45        .chunks_exact(HASH_SIZE)
46        .map(|c| {
47            let mut hash = [0u8; HASH_SIZE];
48            hash.copy_from_slice(c);
49            hash
50        })
51        .collect())
52}
53
54/// The root over a run of hashes that stand for perfect subtrees of equal
55/// size, folded with the RFC 9162 shape. Over 256 leaf hashes this is the
56/// hash the full tile contributes to the level above.
57#[must_use]
58pub fn tile_root(hashes: &[Hash]) -> Hash {
59    rfc9162::root(hashes)
60}
61
62/// The root of the tree whose perfect subtrees have these roots, left to
63/// right, largest first.
64fn fold_right(subtrees: &[Hash]) -> Hash {
65    let mut acc = match subtrees.last() {
66        None => return rfc9162::root(&[]),
67        Some(last) => *last,
68    };
69    for subtree in subtrees[..subtrees.len() - 1].iter().rev() {
70        acc = rfc9162::node_hash(subtree, &acc);
71    }
72    acc
73}
74
75/// The perfect-subtree roots in a run of `width` (1 to 255) hashes of one
76/// level, largest first; each is an aligned chunk whose length is a set bit
77/// of the width.
78fn subtrees_of(hashes: &[Hash]) -> Vec<Hash> {
79    let mut out = Vec::new();
80    let mut at = 0usize;
81    for bit in (0..TILE_HEIGHT).rev() {
82        let chunk = 1usize << bit;
83        if hashes.len() & chunk != 0 {
84            out.push(rfc9162::root(&hashes[at..at + chunk]));
85            at += chunk;
86        }
87    }
88    out
89}
90
91/// The growing edge of a log: for each level, the hashes of its partial tile.
92/// Pushing a leaf fills level 0; a level that reaches 256 hashes becomes a
93/// full tile and contributes one hash to the level above.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Frontier {
96    size: u64,
97    levels: Vec<Vec<Hash>>,
98}
99
100impl Default for Frontier {
101    fn default() -> Self {
102        Self::empty()
103    }
104}
105
106impl Frontier {
107    /// The frontier of the empty tree.
108    #[must_use]
109    pub fn empty() -> Self {
110        Self {
111            size: 0,
112            levels: Vec::new(),
113        }
114    }
115
116    /// The frontier of a tree of `size` leaves, read from the partial tiles
117    /// the store holds for that size. Refused when a partial tile is missing
118    /// or has another width.
119    pub fn load(store: &impl TileStore, size: u64) -> Result<Self> {
120        let layout = Layout::new(size);
121        let mut levels = Vec::new();
122        for level in 0..layout.levels() {
123            match layout.partial_at(level) {
124                Some(tile) => levels.push(read_tile(store, &tile)?),
125                None => levels.push(Vec::new()),
126            }
127        }
128        Ok(Self { size, levels })
129    }
130
131    /// Leaves the frontier covers.
132    #[must_use]
133    pub fn size(&self) -> u64 {
134        self.size
135    }
136
137    /// The tree root at this size.
138    #[must_use]
139    pub fn root(&self) -> Hash {
140        let mut subtrees = Vec::new();
141        for level in self.levels.iter().rev() {
142            subtrees.extend(subtrees_of(level));
143        }
144        fold_right(&subtrees)
145    }
146
147    /// Appends one leaf hash. Every full tile the append completes is pushed
148    /// to `completed`, level 0 first, with its hashes.
149    pub fn push(&mut self, leaf: Hash, completed: &mut Vec<(Tile, Vec<Hash>)>) {
150        self.size += 1;
151        if self.levels.is_empty() {
152            self.levels.push(Vec::with_capacity(TILE_WIDTH as usize));
153        }
154        self.levels[0].push(leaf);
155        let mut level = 0usize;
156        while self.levels[level].len() as u64 == TILE_WIDTH {
157            let hashes = std::mem::take(&mut self.levels[level]);
158            let top = tile_root(&hashes);
159            let shift = (level as u32 + 1) * TILE_HEIGHT;
160            let index = (self.size >> shift) - 1;
161            completed.push((
162                Tile {
163                    level: level as u8,
164                    index,
165                    width: TILE_WIDTH as u16,
166                },
167                hashes,
168            ));
169            if self.levels.len() == level + 1 {
170                self.levels.push(Vec::with_capacity(TILE_WIDTH as usize));
171            }
172            self.levels[level + 1].push(top);
173            level += 1;
174        }
175    }
176
177    /// The partial tiles at this size, with their hashes, level 0 first.
178    #[must_use]
179    pub fn partial_tiles(&self) -> Vec<(Tile, &[Hash])> {
180        let layout = Layout::new(self.size);
181        self.levels
182            .iter()
183            .enumerate()
184            .filter(|(_, hashes)| !hashes.is_empty())
185            .map(|(level, hashes)| {
186                let tile = layout
187                    .partial_at(level as u8)
188                    .expect("a non-empty level has a partial tile");
189                (tile, hashes.as_slice())
190            })
191            .collect()
192    }
193}
194
195/// The hashes of `tile`. A partial tile a writer has since widened is read
196/// from the widest partial or the full tile, whose first `width` hashes it
197/// was, since a writer keeps only the widest partial of a tail tile.
198fn read_tile(store: &impl TileStore, tile: &Tile) -> Result<Vec<Hash>> {
199    let path = tile.path();
200    let hashes = match store.get(&path)? {
201        Some(bytes) => decode_tile(&path, &bytes)?,
202        None if !tile.is_full() => {
203            let full = Tile {
204                level: tile.level,
205                index: tile.index,
206                width: TILE_WIDTH as u16,
207            }
208            .path();
209            let mut covering = vec![full.clone()];
210            covering.extend(store.list(&format!("{full}.p"))?);
211            let mut found = None;
212            for candidate in covering {
213                if let Some(bytes) = store.get(&candidate)? {
214                    let mut hashes = decode_tile(&candidate, &bytes)?;
215                    if hashes.len() >= usize::from(tile.width) {
216                        hashes.truncate(usize::from(tile.width));
217                        found = Some(hashes);
218                        break;
219                    }
220                }
221            }
222            found.ok_or_else(|| Error::Corrupt {
223                path: path.clone(),
224                reason: "missing, and no wider tile covers it".to_string(),
225            })?
226        }
227        None => {
228            return Err(Error::Corrupt {
229                path,
230                reason: "missing".to_string(),
231            })
232        }
233    };
234    if hashes.len() != usize::from(tile.width) {
235        return Err(Error::Corrupt {
236            path,
237            reason: format!("{} hashes, width {}", hashes.len(), tile.width),
238        });
239    }
240    Ok(hashes)
241}
242
243/// Reads the tiles a root or a proof needs, and no tile that has not been
244/// checked against the trusted root: the partial tiles together through the
245/// root they fold to, each full tile through its hash in the level above.
246pub struct TileHashReader<S: TileStore> {
247    store: S,
248    size: u64,
249    root: Hash,
250    cache: TileCache,
251    frontier_checked: Cell<bool>,
252}
253
254impl<S: TileStore> TileHashReader<S> {
255    /// A reader over `store` that trusts `root` to be the root at `size`.
256    pub fn new(store: S, size: u64, root: Hash) -> Self {
257        Self {
258            store,
259            size,
260            root,
261            cache: RefCell::new(HashMap::new()),
262            frontier_checked: Cell::new(false),
263        }
264    }
265
266    /// The store behind the reader.
267    pub fn store(&self) -> &S {
268        &self.store
269    }
270
271    /// Leaves the trusted root covers.
272    #[must_use]
273    pub fn size(&self) -> u64 {
274        self.size
275    }
276
277    /// The trusted root.
278    #[must_use]
279    pub fn trusted_root(&self) -> Hash {
280        self.root
281    }
282
283    /// The root recomputed from the partial tiles in the store; an error when
284    /// it is not the trusted one.
285    pub fn root(&self) -> Result<Hash> {
286        self.check_frontier()?;
287        Ok(self.root)
288    }
289
290    fn check_frontier(&self) -> Result<()> {
291        if self.frontier_checked.get() {
292            return Ok(());
293        }
294        let frontier = Frontier::load(&self.store, self.size)?;
295        let root = frontier.root();
296        if root != self.root {
297            return Err(Error::Corrupt {
298                path: "tile".to_string(),
299                reason: format!(
300                    "the partial tiles fold to {} at size {}, the trusted root is {}",
301                    rfc9162::hex(&root),
302                    self.size,
303                    rfc9162::hex(&self.root)
304                ),
305            });
306        }
307        let mut cache = self.cache.borrow_mut();
308        for (tile, hashes) in frontier.partial_tiles() {
309            cache.insert((tile.level, tile.index), Rc::new(hashes.to_vec()));
310        }
311        self.frontier_checked.set(true);
312        Ok(())
313    }
314
315    /// The hashes of the tile at `level` and `index`, checked against the
316    /// tree. A tile the tree of this size does not have is out of range.
317    pub fn tile(&self, level: u8, index: u64) -> Result<Rc<Vec<Hash>>> {
318        if let Some(hashes) = self.cache.borrow().get(&(level, index)) {
319            return Ok(Rc::clone(hashes));
320        }
321        let layout = Layout::new(self.size);
322        let tile = layout.tile(level, index).ok_or_else(|| {
323            Error::OutOfRange(format!("tile {level}/{index} at size {}", self.size))
324        })?;
325        if !tile.is_full() {
326            self.check_frontier()?;
327            return Ok(Rc::clone(
328                self.cache
329                    .borrow()
330                    .get(&(level, index))
331                    .expect("a checked frontier caches every partial tile"),
332            ));
333        }
334        let hashes = read_tile(&self.store, &tile)?;
335        let parent = self.tile(level + 1, index / TILE_WIDTH)?;
336        let expected = parent[(index % TILE_WIDTH) as usize];
337        let top = tile_root(&hashes);
338        if top != expected {
339            return Err(Error::Corrupt {
340                path: tile.path(),
341                reason: format!(
342                    "folds to {}, the level above holds {}",
343                    rfc9162::hex(&top),
344                    rfc9162::hex(&expected)
345                ),
346            });
347        }
348        let hashes = Rc::new(hashes);
349        self.cache
350            .borrow_mut()
351            .insert((level, index), Rc::clone(&hashes));
352        Ok(hashes)
353    }
354
355    /// Stored hash `index` at `level`: the root over leaves
356    /// `[index * 256^level, (index + 1) * 256^level)`.
357    pub fn hash(&self, level: u8, index: u64) -> Result<Hash> {
358        let tile = self.tile(level, index / TILE_WIDTH)?;
359        let offset = (index % TILE_WIDTH) as usize;
360        tile.get(offset).copied().ok_or_else(|| {
361            Error::OutOfRange(format!(
362                "hash {index} at level {level} at size {}",
363                self.size
364            ))
365        })
366    }
367
368    /// The root of the perfect subtree over leaves `[start, start + 2^height)`,
369    /// which must be aligned and inside the tree.
370    pub fn perfect(&self, start: u64, height: u32) -> Result<Hash> {
371        let span = 1u64 << height;
372        if start % span != 0 || start.saturating_add(span) > self.size {
373            return Err(Error::OutOfRange(format!(
374                "perfect subtree at {start} of height {height} at size {}",
375                self.size
376            )));
377        }
378        let level = (height / TILE_HEIGHT) as u8;
379        let within = height % TILE_HEIGHT;
380        let first = start >> (u32::from(level) * TILE_HEIGHT);
381        let count = 1usize << within;
382        let tile = self.tile(level, first / TILE_WIDTH)?;
383        let offset = (first % TILE_WIDTH) as usize;
384        Ok(rfc9162::root(&tile[offset..offset + count]))
385    }
386
387    /// The RFC 9162 root over leaves `[lo, hi)` of this tree, the same bytes
388    /// `rfc9162::root` gives over that slice of leaf hashes.
389    pub fn range_root(&self, lo: u64, hi: u64) -> Result<Hash> {
390        if lo > hi || hi > self.size {
391            return Err(Error::OutOfRange(format!(
392                "leaves {lo}..{hi} at size {}",
393                self.size
394            )));
395        }
396        let width = hi - lo;
397        if width == 0 {
398            return Ok(rfc9162::root(&[]));
399        }
400        if width.is_power_of_two() && lo % width == 0 {
401            return self.perfect(lo, width.trailing_zeros());
402        }
403        let split = split_point(width);
404        let left = self.range_root(lo, lo + split)?;
405        let right = self.range_root(lo + split, hi)?;
406        Ok(rfc9162::node_hash(&left, &right))
407    }
408    /// The audit path for the leaf at `index`, RFC 9162 section 2.1.3,
409    /// assembled from tiles: the bytes `rfc9162::inclusion_proof` gives over
410    /// the flat leaf slice. Nothing for an index past the size.
411    pub fn inclusion_proof(&self, index: u64) -> Result<Option<Vec<Hash>>> {
412        if index >= self.size {
413            return Ok(None);
414        }
415        let mut path = Vec::new();
416        self.audit_path(0, self.size, index, &mut path)?;
417        Ok(Some(path))
418    }
419
420    fn audit_path(&self, lo: u64, hi: u64, index: u64, path: &mut Vec<Hash>) -> Result<()> {
421        if hi - lo == 1 {
422            return Ok(());
423        }
424        let split = lo + split_point(hi - lo);
425        if index < split {
426            self.audit_path(lo, split, index, path)?;
427            path.push(self.range_root(split, hi)?);
428        } else {
429            self.audit_path(split, hi, index, path)?;
430            path.push(self.range_root(lo, split)?);
431        }
432        Ok(())
433    }
434
435    /// The consistency proof that the tree of `old` leaves is a prefix of
436    /// this one, RFC 9162 section 2.1.4, assembled from tiles: the bytes
437    /// `rfc9162::consistency_proof` gives over the flat leaf slice. Nothing
438    /// when `old` is zero or past the size.
439    pub fn consistency_proof(&self, old: u64) -> Result<Option<Vec<Hash>>> {
440        if old == 0 || old > self.size {
441            return Ok(None);
442        }
443        let mut path = Vec::new();
444        self.subproof(0, self.size, old, true, &mut path)?;
445        Ok(Some(path))
446    }
447
448    /// The RFC's SUBPROOF over leaves `[lo, hi)`; `known` is whether the
449    /// reader already holds the root of the subtree being described.
450    fn subproof(
451        &self,
452        lo: u64,
453        hi: u64,
454        old: u64,
455        known: bool,
456        path: &mut Vec<Hash>,
457    ) -> Result<()> {
458        if old == hi - lo {
459            if !known {
460                path.push(self.range_root(lo, hi)?);
461            }
462            return Ok(());
463        }
464        let split = split_point(hi - lo);
465        if old <= split {
466            self.subproof(lo, lo + split, old, known, path)?;
467            path.push(self.range_root(lo + split, hi)?);
468        } else {
469            self.subproof(lo + split, hi, old - split, false, path)?;
470            path.push(self.range_root(lo, lo + split)?);
471        }
472        Ok(())
473    }
474}
475
476/// The largest power of two strictly below `width`, where RFC 9162 divides a
477/// tree of that width.
478#[must_use]
479pub fn split_point(width: u64) -> u64 {
480    let mut split = 1u64;
481    while split * 2 < width {
482        split *= 2;
483    }
484    split
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn a_split_is_the_rfc_one() {
493        assert_eq!(split_point(2), 1);
494        assert_eq!(split_point(3), 2);
495        assert_eq!(split_point(4), 2);
496        assert_eq!(split_point(5), 4);
497        assert_eq!(split_point(257), 256);
498    }
499
500    #[test]
501    fn a_tile_round_trips_and_a_ragged_one_is_refused() {
502        let hashes: Vec<Hash> = (0..3u8).map(|i| [i; 32]).collect();
503        let bytes = encode_tile(&hashes);
504        assert_eq!(decode_tile("t", &bytes).unwrap(), hashes);
505        assert!(decode_tile("t", &bytes[..40]).is_err());
506        assert!(decode_tile("t", &[]).is_err());
507    }
508}