Skip to content

Commit 2e3e177

Browse files
committed
Adds a new trait for disk format upgrades, implements in on a new struct, PruneTrees, and moves the logic for tree deduplication to the trait impl
1 parent b4211aa commit 2e3e177

File tree

2 files changed

+211
-129
lines changed

2 files changed

+211
-129
lines changed

zebra-state/src/service/finalized_state/disk_format/upgrade.rs

Lines changed: 65 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
thread::{self, JoinHandle},
77
};
88

9-
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender, TryRecvError};
9+
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
1010
use semver::Version;
1111
use tracing::Span;
1212

@@ -20,21 +20,59 @@ use zebra_chain::{
2020

2121
use DbFormatChange::*;
2222

23-
use crate::{
24-
constants::latest_version_for_adding_subtrees,
25-
service::finalized_state::{DiskWriteBatch, ZebraDb},
26-
};
23+
use crate::{constants::latest_version_for_adding_subtrees, service::finalized_state::ZebraDb};
2724

2825
pub(crate) mod add_subtrees;
2926
pub(crate) mod cache_genesis_roots;
3027
pub(crate) mod fix_tree_key_type;
28+
pub(crate) mod prune_trees;
3129

3230
#[cfg(not(feature = "indexer"))]
3331
pub(crate) mod drop_tx_locs_by_spends;
3432

3533
#[cfg(feature = "indexer")]
3634
pub(crate) mod track_tx_locs_by_spends;
3735

36+
/// Defines method signature for running disk format upgrades.
37+
pub trait DiskFormatUpgrade {
38+
/// Returns the version at which this upgrade is applied.
39+
fn version(&self) -> Version;
40+
41+
/// Returns the description of this upgrade.
42+
fn description(&self) -> &'static str;
43+
44+
/// Runs disk format upgrade.
45+
fn run(
46+
&self,
47+
initial_tip_height: Height,
48+
db: &ZebraDb,
49+
cancel_receiver: &Receiver<CancelFormatChange>,
50+
);
51+
52+
/// Check that state has been upgraded to this format correctly.
53+
///
54+
/// # Panics
55+
///
56+
/// If the state has not been upgraded to this format correctly.
57+
fn validate(
58+
&self,
59+
_db: &ZebraDb,
60+
_cancel_receiver: &Receiver<CancelFormatChange>,
61+
) -> Result<Result<(), String>, CancelFormatChange> {
62+
Ok(Ok(()))
63+
}
64+
}
65+
66+
fn format_upgrades() -> Vec<Box<dyn DiskFormatUpgrade>> {
67+
vec![
68+
Box::new(prune_trees::PruneTrees),
69+
// TODO:
70+
// Box::new(add_subtrees::AddSubtrees),
71+
// Box::new(cache_genesis_roots::CacheGenesisRoots),
72+
// Box::new(fix_tree_key_type::FixTreeKeyType),
73+
]
74+
}
75+
3876
/// The kind of database format change or validity check we're performing.
3977
#[derive(Clone, Debug, Eq, PartialEq)]
4078
pub enum DbFormatChange {
@@ -474,78 +512,33 @@ impl DbFormatChange {
474512
return Ok(());
475513
};
476514

477-
// Note commitment tree de-duplication database upgrade task.
478-
479-
let version_for_pruning_trees =
480-
Version::parse("25.1.1").expect("Hardcoded version string should be valid.");
481-
482-
// Check if we need to prune the note commitment trees in the database.
483-
if older_disk_version < &version_for_pruning_trees {
484-
let timer = CodeTimer::start();
485-
486-
// Prune duplicate Sapling note commitment trees.
487-
488-
// The last tree we checked.
489-
let mut last_tree = db
490-
.sapling_tree_by_height(&Height(0))
491-
.expect("Checked above that the genesis block is in the database.");
492-
493-
// Run through all the possible duplicate trees in the finalized chain.
494-
// The block after genesis is the first possible duplicate.
495-
for (height, tree) in db.sapling_tree_by_height_range(Height(1)..=initial_tip_height) {
496-
// Return early if there is a cancel signal.
497-
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
498-
return Err(CancelFormatChange);
499-
}
500-
501-
// Delete any duplicate trees.
502-
if tree == last_tree {
503-
let mut batch = DiskWriteBatch::new();
504-
batch.delete_sapling_tree(db, &height);
505-
db.write_batch(batch)
506-
.expect("Deleting Sapling note commitment trees should always succeed.");
507-
}
508-
509-
// Compare against the last tree to find unique trees.
510-
last_tree = tree;
515+
// Apply or validate format upgrades
516+
for upgrade in format_upgrades() {
517+
if older_disk_version >= &upgrade.version() {
518+
upgrade
519+
.validate(db, cancel_receiver)?
520+
.expect("failed to validate db format");
521+
continue;
511522
}
512523

513-
// Prune duplicate Orchard note commitment trees.
514-
515-
// The last tree we checked.
516-
let mut last_tree = db
517-
.orchard_tree_by_height(&Height(0))
518-
.expect("Checked above that the genesis block is in the database.");
519-
520-
// Run through all the possible duplicate trees in the finalized chain.
521-
// The block after genesis is the first possible duplicate.
522-
for (height, tree) in db.orchard_tree_by_height_range(Height(1)..=initial_tip_height) {
523-
// Return early if there is a cancel signal.
524-
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
525-
return Err(CancelFormatChange);
526-
}
527-
528-
// Delete any duplicate trees.
529-
if tree == last_tree {
530-
let mut batch = DiskWriteBatch::new();
531-
batch.delete_orchard_tree(db, &height);
532-
db.write_batch(batch)
533-
.expect("Deleting Orchard note commitment trees should always succeed.");
534-
}
524+
let timer = CodeTimer::start();
535525

536-
// Compare against the last tree to find unique trees.
537-
last_tree = tree;
538-
}
526+
upgrade.run(initial_tip_height, db, cancel_receiver);
539527

540528
// Before marking the state as upgraded, check that the upgrade completed successfully.
541-
Self::check_for_duplicate_trees(db, cancel_receiver)?
542-
.expect("database format is valid after upgrade");
529+
upgrade
530+
.validate(db, cancel_receiver)?
531+
.expect("db should be valid after upgrade");
543532

544533
// Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the
545534
// database is marked, so the upgrade MUST be complete at this point.
546-
Self::mark_as_upgraded_to(db, &version_for_pruning_trees);
535+
info!(
536+
newer_running_version = ?upgrade.version(),
537+
"Zebra automatically upgraded the database format"
538+
);
539+
Self::mark_as_upgraded_to(db, &upgrade.version());
547540

548-
timer.finish(module_path!(), line!(), "deduplicate trees upgrade");
541+
timer.finish(module_path!(), line!(), upgrade.description());
549542
}
550543

551544
// Note commitment subtree creation database upgrade task.
@@ -669,7 +662,10 @@ impl DbFormatChange {
669662
// Do the quick checks first, so we don't have to do this in every detailed check.
670663
results.push(Self::format_validity_checks_quick(db));
671664

672-
results.push(Self::check_for_duplicate_trees(db, cancel_receiver)?);
665+
for upgrade in format_upgrades() {
666+
results.push(upgrade.validate(db, cancel_receiver)?);
667+
}
668+
673669
results.push(add_subtrees::subtree_format_validity_checks_detailed(
674670
db,
675671
cancel_receiver,
@@ -689,66 +685,6 @@ impl DbFormatChange {
689685
Ok(Ok(()))
690686
}
691687

692-
/// Check that note commitment trees were correctly de-duplicated.
693-
//
694-
// TODO: move this method into an deduplication upgrade module file,
695-
// along with the upgrade code above.
696-
#[allow(clippy::unwrap_in_result)]
697-
fn check_for_duplicate_trees(
698-
db: &ZebraDb,
699-
cancel_receiver: &Receiver<CancelFormatChange>,
700-
) -> Result<Result<(), String>, CancelFormatChange> {
701-
// Runtime test: make sure we removed all duplicates.
702-
// We always run this test, even if the state has supposedly been upgraded.
703-
let mut result = Ok(());
704-
705-
let mut prev_height = None;
706-
let mut prev_tree = None;
707-
for (height, tree) in db.sapling_tree_by_height_range(..) {
708-
// Return early if the format check is cancelled.
709-
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
710-
return Err(CancelFormatChange);
711-
}
712-
713-
if prev_tree == Some(tree.clone()) {
714-
result = Err(format!(
715-
"found duplicate sapling trees after running de-duplicate tree upgrade:\
716-
height: {height:?}, previous height: {:?}, tree root: {:?}",
717-
prev_height.unwrap(),
718-
tree.root()
719-
));
720-
error!(?result);
721-
}
722-
723-
prev_height = Some(height);
724-
prev_tree = Some(tree);
725-
}
726-
727-
let mut prev_height = None;
728-
let mut prev_tree = None;
729-
for (height, tree) in db.orchard_tree_by_height_range(..) {
730-
// Return early if the format check is cancelled.
731-
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
732-
return Err(CancelFormatChange);
733-
}
734-
735-
if prev_tree == Some(tree.clone()) {
736-
result = Err(format!(
737-
"found duplicate orchard trees after running de-duplicate tree upgrade:\
738-
height: {height:?}, previous height: {:?}, tree root: {:?}",
739-
prev_height.unwrap(),
740-
tree.root()
741-
));
742-
error!(?result);
743-
}
744-
745-
prev_height = Some(height);
746-
prev_tree = Some(tree);
747-
}
748-
749-
Ok(result)
750-
}
751-
752688
/// Mark a newly created database with the current format version.
753689
///
754690
/// This should be called when a newly created database is opened.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Prunes duplicate Sapling and Orchard note commitment trees from database
2+
3+
use crossbeam_channel::{Receiver, TryRecvError};
4+
5+
use semver::Version;
6+
use zebra_chain::block::Height;
7+
8+
use crate::service::finalized_state::{DiskWriteBatch, ZebraDb};
9+
10+
use super::{CancelFormatChange, DiskFormatUpgrade};
11+
12+
/// Implements [`DiskFormatUpgrade`] for pruning duplicate Sapling and Orchard note commitment trees from database
13+
pub struct PruneTrees;
14+
15+
impl DiskFormatUpgrade for PruneTrees {
16+
fn version(&self) -> Version {
17+
Version::new(25, 1, 1)
18+
}
19+
20+
fn description(&self) -> &'static str {
21+
"deduplicate trees upgrade"
22+
}
23+
24+
fn run(
25+
&self,
26+
initial_tip_height: Height,
27+
db: &ZebraDb,
28+
cancel_receiver: &Receiver<CancelFormatChange>,
29+
) {
30+
// Prune duplicate Sapling note commitment trees.
31+
32+
// The last tree we checked.
33+
let mut last_tree = db
34+
.sapling_tree_by_height(&Height(0))
35+
.expect("Checked above that the genesis block is in the database.");
36+
37+
// Run through all the possible duplicate trees in the finalized chain.
38+
// The block after genesis is the first possible duplicate.
39+
for (height, tree) in db.sapling_tree_by_height_range(Height(1)..=initial_tip_height) {
40+
// Return early if there is a cancel signal.
41+
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
42+
return;
43+
}
44+
45+
// Delete any duplicate trees.
46+
if tree == last_tree {
47+
let mut batch = DiskWriteBatch::new();
48+
batch.delete_sapling_tree(db, &height);
49+
db.write_batch(batch)
50+
.expect("Deleting Sapling note commitment trees should always succeed.");
51+
}
52+
53+
// Compare against the last tree to find unique trees.
54+
last_tree = tree;
55+
}
56+
57+
// Prune duplicate Orchard note commitment trees.
58+
59+
// The last tree we checked.
60+
let mut last_tree = db
61+
.orchard_tree_by_height(&Height(0))
62+
.expect("Checked above that the genesis block is in the database.");
63+
64+
// Run through all the possible duplicate trees in the finalized chain.
65+
// The block after genesis is the first possible duplicate.
66+
for (height, tree) in db.orchard_tree_by_height_range(Height(1)..=initial_tip_height) {
67+
// Return early if there is a cancel signal.
68+
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
69+
return;
70+
}
71+
72+
// Delete any duplicate trees.
73+
if tree == last_tree {
74+
let mut batch = DiskWriteBatch::new();
75+
batch.delete_orchard_tree(db, &height);
76+
db.write_batch(batch)
77+
.expect("Deleting Orchard note commitment trees should always succeed.");
78+
}
79+
80+
// Compare against the last tree to find unique trees.
81+
last_tree = tree;
82+
}
83+
}
84+
85+
/// Check that note commitment trees were correctly de-duplicated.
86+
///
87+
/// # Panics
88+
///
89+
/// If a duplicate tree is found.
90+
#[allow(clippy::unwrap_in_result)]
91+
fn validate(
92+
&self,
93+
db: &ZebraDb,
94+
cancel_receiver: &Receiver<CancelFormatChange>,
95+
) -> Result<Result<(), String>, CancelFormatChange> {
96+
// Runtime test: make sure we removed all duplicates.
97+
// We always run this test, even if the state has supposedly been upgraded.
98+
let mut result = Ok(());
99+
100+
let mut prev_height = None;
101+
let mut prev_tree = None;
102+
for (height, tree) in db.sapling_tree_by_height_range(..) {
103+
// Return early if the format check is cancelled.
104+
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
105+
return Err(CancelFormatChange);
106+
}
107+
108+
if prev_tree == Some(tree.clone()) {
109+
result = Err(format!(
110+
"found duplicate sapling trees after running de-duplicate tree upgrade:\
111+
height: {height:?}, previous height: {:?}, tree root: {:?}",
112+
prev_height.unwrap(),
113+
tree.root()
114+
));
115+
error!(?result);
116+
}
117+
118+
prev_height = Some(height);
119+
prev_tree = Some(tree);
120+
}
121+
122+
let mut prev_height = None;
123+
let mut prev_tree = None;
124+
for (height, tree) in db.orchard_tree_by_height_range(..) {
125+
// Return early if the format check is cancelled.
126+
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) {
127+
return Err(CancelFormatChange);
128+
}
129+
130+
if prev_tree == Some(tree.clone()) {
131+
result = Err(format!(
132+
"found duplicate orchard trees after running de-duplicate tree upgrade:\
133+
height: {height:?}, previous height: {:?}, tree root: {:?}",
134+
prev_height.unwrap(),
135+
tree.root()
136+
));
137+
error!(?result);
138+
}
139+
140+
prev_height = Some(height);
141+
prev_tree = Some(tree);
142+
}
143+
144+
Ok(result)
145+
}
146+
}

0 commit comments

Comments
 (0)