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.
56use crossbeam_channel::{Receiver, TryRecvError};
7use zebra_chain::{block::Height, sprout};
89use crate::service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb};
1011use super::CancelFormatChange;
1213/// 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> {
27let sprout_genesis_tree = sprout::tree::NoteCommitmentTree::default();
28let sprout_tip_tree = upgrade_db.sprout_tree_for_tip();
2930let sapling_genesis_tree = upgrade_db
31 .sapling_tree_by_height(&Height(0))
32 .expect("caller has checked for genesis block");
33let orchard_genesis_tree = upgrade_db
34 .orchard_tree_by_height(&Height(0))
35 .expect("caller has checked for genesis block");
3637// Writing the trees back to the database automatically caches their roots.
38let mut batch = DiskWriteBatch::new();
3940// Fix the cached root of the Sprout genesis tree in its anchors column family.
4142 // 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.
44batch.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.
46batch.update_sprout_tree(upgrade_db, &sprout_tip_tree);
4748 batch.create_sapling_tree(upgrade_db, &Height(0), &sapling_genesis_tree);
49 batch.create_orchard_tree(upgrade_db, &Height(0), &orchard_genesis_tree);
5051// Return before we write if the upgrade is cancelled.
52if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
53return Err(CancelFormatChange);
54 }
5556 upgrade_db
57 .write_batch(batch)
58 .expect("updating tree cached roots should always succeed");
5960Ok(())
61}
6263/// 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.
73if db.is_empty() {
74return Ok(());
75 }
7677let sprout_genesis_tree = sprout::tree::NoteCommitmentTree::default();
78let sprout_genesis_tree = db
79 .sprout_tree_by_anchor(&sprout_genesis_tree.root())
80 .expect("just checked for genesis block");
81let sprout_tip_tree = db.sprout_tree_for_tip();
8283let sapling_genesis_tree = db
84 .sapling_tree_by_height(&Height(0))
85 .expect("just checked for genesis block");
86let orchard_genesis_tree = db
87 .orchard_tree_by_height(&Height(0))
88 .expect("just checked for genesis block");
8990// Check the entire format before returning any errors.
91let sprout_result = sprout_genesis_tree
92 .cached_root()
93 .ok_or("no cached root in sprout genesis tree");
94let sprout_tip_result = sprout_tip_tree
95 .cached_root()
96 .ok_or("no cached root in sprout tip tree");
9798let sapling_result = sapling_genesis_tree
99 .cached_root()
100 .ok_or("no cached root in sapling genesis tree");
101let orchard_result = orchard_genesis_tree
102 .cached_root()
103 .ok_or("no cached root in orchard genesis tree");
104105if sprout_result.is_err()
106 || sprout_tip_result.is_err()
107 || sapling_result.is_err()
108 || orchard_result.is_err()
109 {
110let err = Err(format!(
111"missing cached genesis root: sprout: {sprout_result:?}, {sprout_tip_result:?} \
112 sapling: {sapling_result:?}, orchard: {orchard_result:?}"
113));
114warn!(?err);
115return err;
116 }
117118Ok(())
119}
120121/// 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.
132let mut result = quick_check(db);
133134for (root, tree) in db.sprout_trees_full_map() {
135// Return early if the format check is cancelled.
136if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
137return Err(CancelFormatChange);
138 }
139140if 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));
145error!(?result);
146 }
147 }
148149for (height, tree) in db.sapling_tree_by_height_range(..) {
150// Return early if the format check is cancelled.
151if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
152return Err(CancelFormatChange);
153 }
154155if 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));
160error!(?result);
161 }
162 }
163164for (height, tree) in db.orchard_tree_by_height_range(..) {
165// Return early if the format check is cancelled.
166if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
167return Err(CancelFormatChange);
168 }
169170if 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));
175error!(?result);
176 }
177 }
178179Ok(result)
180}