zebra_state/service/finalized_state/disk_format/upgrade/
cache_genesis_roots.rs

1//! Updating the genesis note commitment trees to cache their roots.
2//!
3//! This reduces CPU usage when the genesis tree roots are used for transaction validation.
4//! Since mempool transactions are cheap to create, this is a potential remote denial of service.
5
6use crossbeam_channel::{Receiver, TryRecvError};
7use zebra_chain::{block::Height, sprout};
8
9use crate::service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb};
10
11use super::CancelFormatChange;
12
13/// Runs disk format upgrade for changing the sprout and history tree key types.
14///
15/// Returns `Ok` if the upgrade completed, and `Err` if it was cancelled.
16///
17/// # Panics
18///
19/// If the state is empty.
20#[allow(clippy::unwrap_in_result)]
21#[instrument(skip(upgrade_db, cancel_receiver))]
22pub fn run(
23    _initial_tip_height: Height,
24    upgrade_db: &ZebraDb,
25    cancel_receiver: &Receiver<CancelFormatChange>,
26) -> Result<(), CancelFormatChange> {
27    let sprout_genesis_tree = sprout::tree::NoteCommitmentTree::default();
28    let sprout_tip_tree = upgrade_db.sprout_tree_for_tip();
29
30    let sapling_genesis_tree = upgrade_db
31        .sapling_tree_by_height(&Height(0))
32        .expect("caller has checked for genesis block");
33    let orchard_genesis_tree = upgrade_db
34        .orchard_tree_by_height(&Height(0))
35        .expect("caller has checked for genesis block");
36
37    // Writing the trees back to the database automatically caches their roots.
38    let mut batch = DiskWriteBatch::new();
39
40    // Fix the cached root of the Sprout genesis tree in its anchors column family.
41
42    // It's ok to write the genesis tree to the tip tree index, because it's overwritten by
43    // the actual tip before the batch is written to the database.
44    batch.update_sprout_tree(upgrade_db, &sprout_genesis_tree);
45    // This method makes sure the sprout tip tree has a cached root, even if it's the genesis tree.
46    batch.update_sprout_tree(upgrade_db, &sprout_tip_tree);
47
48    batch.create_sapling_tree(upgrade_db, &Height(0), &sapling_genesis_tree);
49    batch.create_orchard_tree(upgrade_db, &Height(0), &orchard_genesis_tree);
50
51    // Return before we write if the upgrade is cancelled.
52    if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
53        return Err(CancelFormatChange);
54    }
55
56    upgrade_db
57        .write_batch(batch)
58        .expect("updating tree cached roots should always succeed");
59
60    Ok(())
61}
62
63/// Quickly check that the genesis trees and sprout tip tree have cached roots.
64///
65/// This allows us to fail the upgrade quickly in tests and during development,
66/// rather than waiting to see if it failed.
67///
68/// # Panics
69///
70/// If the state is empty.
71pub fn quick_check(db: &ZebraDb) -> Result<(), String> {
72    // An empty database doesn't have any trees, so its format is trivially correct.
73    if db.is_empty() {
74        return Ok(());
75    }
76
77    let sprout_genesis_tree = sprout::tree::NoteCommitmentTree::default();
78    let sprout_genesis_tree = db
79        .sprout_tree_by_anchor(&sprout_genesis_tree.root())
80        .expect("just checked for genesis block");
81    let sprout_tip_tree = db.sprout_tree_for_tip();
82
83    let sapling_genesis_tree = db
84        .sapling_tree_by_height(&Height(0))
85        .expect("just checked for genesis block");
86    let orchard_genesis_tree = db
87        .orchard_tree_by_height(&Height(0))
88        .expect("just checked for genesis block");
89
90    // Check the entire format before returning any errors.
91    let sprout_result = sprout_genesis_tree
92        .cached_root()
93        .ok_or("no cached root in sprout genesis tree");
94    let sprout_tip_result = sprout_tip_tree
95        .cached_root()
96        .ok_or("no cached root in sprout tip tree");
97
98    let sapling_result = sapling_genesis_tree
99        .cached_root()
100        .ok_or("no cached root in sapling genesis tree");
101    let orchard_result = orchard_genesis_tree
102        .cached_root()
103        .ok_or("no cached root in orchard genesis tree");
104
105    if sprout_result.is_err()
106        || sprout_tip_result.is_err()
107        || sapling_result.is_err()
108        || orchard_result.is_err()
109    {
110        let err = Err(format!(
111            "missing cached genesis root: sprout: {sprout_result:?}, {sprout_tip_result:?} \
112             sapling: {sapling_result:?}, orchard: {orchard_result:?}"
113        ));
114        warn!(?err);
115        return err;
116    }
117
118    Ok(())
119}
120
121/// Detailed check that all trees have cached roots.
122///
123/// # Panics
124///
125/// If the state is empty.
126pub fn detailed_check(
127    db: &ZebraDb,
128    cancel_receiver: &Receiver<CancelFormatChange>,
129) -> Result<Result<(), String>, CancelFormatChange> {
130    // This is redundant in some code paths, but not in others. But it's quick anyway.
131    // Check the entire format before returning any errors.
132    let mut result = quick_check(db);
133
134    for (root, tree) in db.sprout_trees_full_map() {
135        // Return early if the format check is cancelled.
136        if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
137            return Err(CancelFormatChange);
138        }
139
140        if tree.cached_root().is_none() {
141            result = Err(format!(
142                "found un-cached sprout tree root after running genesis tree root fix \
143                 {root:?}"
144            ));
145            error!(?result);
146        }
147    }
148
149    for (height, tree) in db.sapling_tree_by_height_range(..) {
150        // Return early if the format check is cancelled.
151        if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
152            return Err(CancelFormatChange);
153        }
154
155        if tree.cached_root().is_none() {
156            result = Err(format!(
157                "found un-cached sapling tree root after running genesis tree root fix \
158                 {height:?}"
159            ));
160            error!(?result);
161        }
162    }
163
164    for (height, tree) in db.orchard_tree_by_height_range(..) {
165        // Return early if the format check is cancelled.
166        if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
167            return Err(CancelFormatChange);
168        }
169
170        if tree.cached_root().is_none() {
171            result = Err(format!(
172                "found un-cached orchard tree root after running genesis tree root fix \
173                 {height:?}"
174            ));
175            error!(?result);
176        }
177    }
178
179    Ok(result)
180}