Skip to main content

pepper_sync/
shardtree_ext.rs

1//! Domain-typed outcomes for shardtree's boolean operations.
2//!
3//! shardtree reports several operations as `Ok(bool)`, where `false`
4//! conflates distinct situations that demand different responses. That
5//! conflation caused a real defect: a reorg truncation read the `false`
6//! from [`ShardTree::truncate_to_checkpoint`] on an empty migrated
7//! ironwood tree as corruption and wiped the wallet. This module is the
8//! crate's single boundary with those boolean APIs: each wrapper names
9//! every outcome in a closed enum, so callers must match on meanings
10//! rather than interpret a bit.
11//!
12//! Raw calls to the wrapped operations are confined to this module. The
13//! `raw_boolean_calls_are_confined_to_this_module` test enforces the
14//! boundary.
15
16use shardtree::ShardTree;
17use shardtree::error::ShardTreeError;
18use shardtree::store::memory::MemoryShardStore;
19use std::convert::Infallible;
20
21use incrementalmerkletree::Hashable;
22use zcash_protocol::consensus::BlockHeight;
23
24/// Outcome of appending a checkpoint that records the tree's current
25/// frontier position at a new height.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum CheckpointAppendOutcome {
28    /// The association between the id and the current frontier position
29    /// was recorded.
30    Appended,
31    /// The id is not above the store's newest checkpoint. The tree is
32    /// unchanged. Appending stamps the *current* frontier, so recording
33    /// it at or below an already-stamped height would duplicate or
34    /// falsify the height-to-frontier record. Checkpoints for past
35    /// heights travel with their own tree states through the
36    /// order-agnostic channels instead ([`ShardTree::insert_tree`]'s
37    /// per-subtree checkpoint maps, and the store-level
38    /// `ShardStore::add_checkpoint`).
39    NotAboveNewest,
40}
41
42/// Outcome of rolling a tree back to a checkpoint.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum RollbackOutcome {
45    /// The tree was truncated to the checkpoint's state.
46    RolledBack,
47    /// No checkpoint exists with the given id. The tree is unchanged.
48    NoSuchCheckpoint,
49}
50
51/// The boundary wrapper over shardtree's boolean operations, exported so
52/// downstream crates route through it too (zingolib's legacy wallet
53/// import does. Its own source-walk test enforces its confinement).
54pub trait ShardTreeExt {
55    /// As [`ShardTree::checkpoint`], with the boolean classified: append
56    /// a checkpoint recording the tree's current frontier at the id.
57    fn append_checkpoint(
58        &mut self,
59        checkpoint_id: BlockHeight,
60    ) -> Result<CheckpointAppendOutcome, ShardTreeError<Infallible>>;
61
62    /// As [`ShardTree::truncate_to_checkpoint`], with the boolean
63    /// classified.
64    fn rollback_to_checkpoint(
65        &mut self,
66        checkpoint_id: BlockHeight,
67    ) -> Result<RollbackOutcome, ShardTreeError<Infallible>>;
68}
69
70impl<H, const DEPTH: u8, const SHARD_HEIGHT: u8> ShardTreeExt
71    for ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>
72where
73    H: Hashable + Clone + PartialEq,
74{
75    fn append_checkpoint(
76        &mut self,
77        checkpoint_id: BlockHeight,
78    ) -> Result<CheckpointAppendOutcome, ShardTreeError<Infallible>> {
79        Ok(if self.checkpoint(checkpoint_id)? {
80            CheckpointAppendOutcome::Appended
81        } else {
82            CheckpointAppendOutcome::NotAboveNewest
83        })
84    }
85
86    fn rollback_to_checkpoint(
87        &mut self,
88        checkpoint_id: BlockHeight,
89    ) -> Result<RollbackOutcome, ShardTreeError<Infallible>> {
90        Ok(if self.truncate_to_checkpoint(&checkpoint_id)? {
91            RollbackOutcome::RolledBack
92        } else {
93            RollbackOutcome::NoSuchCheckpoint
94        })
95    }
96}
97
98#[cfg(test)]
99mod test {
100    use super::*;
101
102    /// The boundary rule: every raw call to a wrapped boolean operation
103    /// lives in this module. A raw call elsewhere reintroduces the
104    /// boolean-conflation class this module retires, so the tree walk
105    /// fails the build the moment one appears.
106    #[test]
107    fn raw_boolean_calls_are_confined_to_this_module() {
108        let src_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
109        let mut rust_sources = Vec::new();
110        collect_rust_sources(&src_root, &mut rust_sources);
111        assert!(!rust_sources.is_empty());
112
113        let raw_patterns = [
114            ".checkpoint(",
115            ".truncate_to_checkpoint(",
116            ".truncate_to_checkpoint_depth(",
117        ];
118        for path in rust_sources {
119            if path.ends_with("shardtree_ext.rs") {
120                continue;
121            }
122            let source = std::fs::read_to_string(&path).unwrap();
123            for pattern in raw_patterns {
124                assert!(
125                    !source.contains(pattern),
126                    "{} calls a raw shardtree boolean operation ({pattern}); \
127                     route it through crate::shardtree_ext instead",
128                    path.display(),
129                );
130            }
131        }
132    }
133
134    fn collect_rust_sources(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
135        for entry in std::fs::read_dir(dir).unwrap() {
136            let path = entry.unwrap().path();
137            if path.is_dir() {
138                collect_rust_sources(&path, out);
139            } else if path.extension().is_some_and(|extension| extension == "rs") {
140                out.push(path);
141            }
142        }
143    }
144
145    /// The wrappers report the store's actual behavior: a fresh tree
146    /// accepts an initial checkpoint, refuses a non-advancing one, rolls
147    /// back to an existing checkpoint, and reports a missing one.
148    #[test]
149    fn outcomes_match_store_behavior() {
150        let mut tree: ShardTree<
151            MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>,
152            { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
153            { crate::witness::SHARD_HEIGHT },
154        > = ShardTree::new(MemoryShardStore::empty(), 10);
155
156        let five = BlockHeight::from_u32(5);
157        assert_eq!(
158            tree.append_checkpoint(five).unwrap(),
159            CheckpointAppendOutcome::Appended
160        );
161        assert_eq!(
162            tree.append_checkpoint(five).unwrap(),
163            CheckpointAppendOutcome::NotAboveNewest
164        );
165        assert_eq!(
166            tree.rollback_to_checkpoint(five).unwrap(),
167            RollbackOutcome::RolledBack
168        );
169        assert_eq!(
170            tree.rollback_to_checkpoint(BlockHeight::from_u32(4))
171                .unwrap(),
172            RollbackOutcome::NoSuchCheckpoint
173        );
174    }
175}