Skip to main content

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

1//! Backfill the genesis Ironwood note commitment tree and anchor for existing databases.
2//!
3//! The NU6.3 Ironwood column families are created empty when a v28 database is opened. A node that
4//! synced from genesis under v28 writes the (empty) Ironwood tree and its anchor at the genesis
5//! height during the genesis block commit. A node upgrading an older database committed its genesis
6//! block long ago, under code that never wrote an Ironwood tree, so the `ironwood_note_commitment_tree`
7//! and `ironwood_anchors` column families would otherwise stay empty — which makes
8//! [`ZebraDb::ironwood_tree_for_tip`](crate::service::finalized_state::ZebraDb::ironwood_tree_for_tip)
9//! panic on the next block and leaves the genesis Ironwood anchor missing for NU6.3 anchor
10//! validation.
11//!
12//! This migration backfills the empty Ironwood tree (and its anchor) at the genesis height so the
13//! on-disk state matches a genesis-synced node. It is a no-op for genesis-synced and already-upgraded
14//! databases. The Ironwood pool reuses the Orchard note commitment tree type.
15
16use crossbeam_channel::{Receiver, TryRecvError};
17use semver::Version;
18
19use zebra_chain::{block::Height, orchard};
20
21use crate::service::finalized_state::{DiskWriteBatch, ZebraDb};
22
23use super::{CancelFormatChange, DiskFormatUpgrade};
24
25/// Implements [`DiskFormatUpgrade`] for backfilling the genesis Ironwood tree and anchor.
26pub struct Upgrade;
27
28impl DiskFormatUpgrade for Upgrade {
29    fn version(&self) -> Version {
30        Version::new(28, 0, 0)
31    }
32
33    fn description(&self) -> &'static str {
34        "add ironwood shielded pool state (genesis tree and anchor backfill)"
35    }
36
37    #[allow(clippy::unwrap_in_result)]
38    fn run(
39        &self,
40        _initial_tip_height: Height,
41        db: &ZebraDb,
42        cancel_receiver: &Receiver<CancelFormatChange>,
43    ) -> Result<(), CancelFormatChange> {
44        check_cancelled(cancel_receiver)?;
45
46        // Nothing to do for empty databases, or databases that already have the genesis tree
47        // (genesis-synced under v28, or a previous run of this migration).
48        if db.finalized_tip_height().is_none() || has_genesis_ironwood_tree(db) {
49            return Ok(());
50        }
51
52        // Write the empty Ironwood tree (and its anchor) at the genesis height, matching what a
53        // genesis-synced v28 node writes during the genesis block commit.
54        let ironwood_tree = orchard::tree::NoteCommitmentTree::default();
55        let mut batch = DiskWriteBatch::new();
56        batch.create_ironwood_tree(db, &Height::MIN, &ironwood_tree);
57
58        check_cancelled(cancel_receiver)?;
59
60        db.write_batch(batch)
61            .expect("backfilling the genesis Ironwood tree should always succeed");
62
63        Ok(())
64    }
65
66    fn validate(
67        &self,
68        db: &ZebraDb,
69        _cancel_receiver: &Receiver<CancelFormatChange>,
70    ) -> Result<Result<(), String>, CancelFormatChange> {
71        // Empty databases have no genesis tree to check.
72        if db.finalized_tip_height().is_none() {
73            return Ok(Ok(()));
74        }
75
76        if !has_genesis_ironwood_tree(db) {
77            return Ok(Err(
78                "missing Ironwood note commitment tree for the genesis height".to_string(),
79            ));
80        }
81
82        let ironwood_tree = db.ironwood_tree_for_tip();
83        if !db.contains_ironwood_anchor(&ironwood_tree.root()) {
84            return Ok(Err(
85                "missing Ironwood anchor for the finalized tip's Ironwood tree".to_string(),
86            ));
87        }
88
89        Ok(Ok(()))
90    }
91}
92
93/// Returns `true` if the database has an Ironwood note commitment tree at or below the genesis height.
94fn has_genesis_ironwood_tree(db: &ZebraDb) -> bool {
95    db.ironwood_tree_by_height_range(..=Height::MIN)
96        .next()
97        .is_some()
98}
99
100fn check_cancelled(
101    cancel_receiver: &Receiver<CancelFormatChange>,
102) -> Result<(), CancelFormatChange> {
103    match cancel_receiver.try_recv() {
104        Err(TryRecvError::Empty) => Ok(()),
105        _ => Err(CancelFormatChange),
106    }
107}