use std::{
io::{stderr, stdout, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc, LazyLock,
},
};
use zakura_chain::{
block, ironwood, orchard,
parallel::tree::NoteCommitmentTrees,
parameters::{Network, NetworkUpgrade},
sapling,
};
use zakura_db::{
block::{RetentionPlan, ZAKURA_HEADER_BODY_SIZE_BY_HEIGHT},
chain::BLOCK_INFO,
transparent::{BALANCE_BY_TRANSPARENT_ADDR, TX_LOC_BY_SPENT_OUT_LOC},
};
use crate::{
constants::{state_database_format_version_in_code, STATE_DATABASE_KIND},
error::CommitCheckpointVerifiedError,
request::{FinalizableBlock, FinalizedBlock, Treestate},
service::{check, QueuedCheckpointVerified},
CheckpointVerifiedBlock, Config, StateInitError, ValidateContextError,
};
macro_rules! timed_commit_phase {
($name:expr, $body:expr) => {{
#[cfg(feature = "commit-metrics")]
let _start = std::time::Instant::now();
let result = $body;
#[cfg(feature = "commit-metrics")]
metrics::histogram!($name).record(_start.elapsed().as_secs_f64());
result
}};
}
static COMMIT_COMPUTE_POOL: LazyLock<rayon::ThreadPool> = LazyLock::new(|| {
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|i| format!("commit-compute-{i}"))
.build()
.expect("rayon thread pool configuration is valid")
});
pub mod column_family;
pub(crate) mod commitment_aux;
pub(crate) mod commitment_aux_verify;
mod disk_db;
mod disk_format;
pub(super) mod header_chain;
pub mod treestate_artifact;
pub mod treestate_export;
mod vct;
pub mod vct_treestate_audit;
mod zakura_db;
pub(crate) use vct::embedded_last_checkpoint_leaf_counts;
use vct::{VctCommitState, VctState, VctWriteData};
#[cfg(any(test, feature = "proptest-impl"))]
mod arbitrary;
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "proptest-impl"))]
pub mod vct_fast_sync_fixture;
#[allow(unused_imports)]
pub use column_family::{TypedColumnFamily, WriteTypedBatch};
pub(crate) use commitment_aux::serve_block_roots;
pub use commitment_aux::{
produce_final_frontiers_bytes, produce_settled_final_frontiers_bytes,
FinalFrontiersGenerationError,
};
#[allow(unused_imports)]
pub use disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk};
#[allow(unused_imports)]
pub use disk_format::{
FallibleDiskValue, FromDisk, IntoDisk, OutputLocation, RawBytes, TransactionIndex,
TransactionLocation, MAX_ON_DISK_HEIGHT,
};
#[cfg(feature = "internal-bench")]
pub use header_chain::{
benchmark_finality_witness, FinalityWitnessBenchmarkReport, FinalityWitnessBenchmarkSample,
};
#[cfg(any(test, feature = "header-fuzz"))]
pub use header_chain::{replay_recovery_rows_bytes, RecoveryRowsReplaySummary};
pub(crate) use treestate_artifact::{
embedded_historical_frontier_artifact, embedded_historical_subtrees,
};
pub use treestate_artifact::{
verify_subtree_artifact, FrontierArtifact, FrontierEntry, SubtreeArtifact, SubtreeRecord,
TreestateArtifactError, VerifiedSubtreeCounts,
};
pub use treestate_export::{
export_frontier_grid_to, produce_release_treestate_artifacts, FrontierGridExport,
FrontierGridExportError, GridSpacing, ReleaseTreestateArtifacts,
ReleaseTreestateArtifactsError,
};
pub use vct::{validate_final_frontiers_bytes, FinalFrontiersValidationError, VctSuccessorWitness};
pub(crate) use vct::{VctAuthenticationProof, VctAuxiliaryFailureAttribution, VctAuxiliaryWindow};
pub use vct_treestate_audit::{
derived_roots_in_display_order, inventory, inventory_with_scans, measure_derivations,
replay_inputs, verify_subtrees_against_stored, DerivationSample, ReplayInputs,
SubtreeVerification, VctTreestateInventory,
};
#[allow(unused_imports)]
pub use zakura_db::commitment_roots_db::{CommitmentRootIndexIssue, COMMITMENT_ROOTS_BY_HEIGHT};
#[allow(unused_imports)]
pub use zakura_db::highest_completed_checkpoint::*;
pub use zakura_db::ZakuraDb;
#[cfg(any(test, feature = "proptest-impl"))]
pub use disk_format::KV;
pub use disk_format::upgrade::restorable_db_versions;
pub use zakura_db::prune::{
preview_prune_finalized_state, prune_finalized_state, PruneFinalizedStateError,
PruneFinalizedStateOptions, PruneFinalizedStateSummary,
};
pub use zakura_db::rollback::{
preview_rollback_finalized_state, rollback_finalized_state, RollbackBackupSummary,
RollbackFinalizedStateError, RollbackFinalizedStateOptions, RollbackFinalizedStateSummary,
};
pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[
"hash_by_height",
"height_by_hash",
"block_header_by_height",
"zakura_header_hash_by_height",
"zakura_header_height_by_hash",
"zakura_header_by_height",
ZAKURA_HEADER_BODY_SIZE_BY_HEIGHT,
HEADER_NODE_BY_HASH,
HEADER_CONSENSUS_INVALID_BODY_TOMBSTONE,
HEADER_BODY_EVIDENCE_AUTHORITY,
HEADER_CHILD,
HEADER_SELECTED,
HEADER_VERIFIED,
HEADER_ELIGIBILITY_ROOT,
HEADER_AUX_DELIVERY,
HEADER_DEFERRED,
HEADER_FINALITY_HISTORY,
HEADER_FINALITY_WITNESS,
HEADER_VALIDATION_CONTEXT,
HEADER_ENGINE_META,
"tx_by_loc",
"hash_by_tx_loc",
"tx_loc_by_hash",
BALANCE_BY_TRANSPARENT_ADDR,
"tx_loc_by_transparent_addr_loc",
"utxo_by_out_loc",
"utxo_loc_by_transparent_addr_loc",
TX_LOC_BY_SPENT_OUT_LOC,
"sprout_nullifiers",
"sprout_anchors",
"sprout_note_commitment_tree",
"sapling_nullifiers",
"sapling_anchors",
"sapling_note_commitment_tree",
"sapling_note_commitment_subtree",
"orchard_nullifiers",
"orchard_anchors",
"orchard_note_commitment_tree",
"orchard_note_commitment_subtree",
"ironwood_nullifiers",
"ironwood_anchors",
"ironwood_note_commitment_tree",
"ironwood_note_commitment_subtree",
"history_tree",
"tip_chain_value_pool",
BLOCK_INFO,
COMMITMENT_ROOTS_BY_HEIGHT,
PRUNING_METADATA,
VCT_SYNC_METADATA,
VCT_UPGRADE_METADATA,
];
pub const HEADER_NODE_BY_HASH: &str = "header_node_by_hash_v1";
pub const HEADER_CONSENSUS_INVALID_BODY_TOMBSTONE: &str = "header_consensus_invalid_tombstone_v1";
pub const HEADER_BODY_EVIDENCE_AUTHORITY: &str = "header_body_evidence_authority_v1";
pub const HEADER_CHILD: &str = "header_child_v1";
pub const HEADER_SELECTED: &str = "header_selected_v1";
pub const HEADER_VERIFIED: &str = "header_verified_v1";
pub const HEADER_ELIGIBILITY_ROOT: &str = "header_eligibility_root_v1";
pub const HEADER_AUX_DELIVERY: &str = "header_aux_delivery_v1";
pub const HEADER_DEFERRED: &str = "header_deferred_v1";
pub const HEADER_FINALITY_HISTORY: &str = "header_finality_history_v1";
pub const HEADER_FINALITY_WITNESS: &str = "header_finality_witness_v1";
pub const HEADER_VALIDATION_CONTEXT: &str = "header_validation_context_v1";
pub const HEADER_ENGINE_META: &str = "header_engine_meta_v1";
pub const PRUNING_METADATA: &str = "pruning_metadata";
pub const VCT_SYNC_METADATA: &str = "vct_sync_metadata";
pub const VCT_UPGRADE_METADATA: &str = "vct_upgrade_metadata";
#[derive(Clone, Debug)]
pub struct FinalizedState {
debug_stop_at_height: Option<block::Height>,
checkpoint_raw_tx_retention_start: Option<block::Height>,
checkpoint_raw_tx_archive_backlog: Arc<AtomicBool>,
pub db: ZakuraDb,
vct: VctCommitState,
}
impl FinalizedState {
pub fn new(config: &Config, network: &Network) -> Result<Self, StateInitError> {
Self::new_with_debug(config, network, false, false)
}
pub fn new_writable(config: &Config, network: &Network) -> Self {
Self::new_with_debug(config, network, false, false).expect(
"opening the read-write finalized state database failed; check that the \
state cache directory is writable and not locked by another Zakura instance, \
and that there is free disk space",
)
}
#[allow(clippy::unwrap_in_result)]
pub(crate) fn new_with_debug(
config: &Config,
network: &Network,
debug_skip_format_upgrades: bool,
read_only: bool,
) -> Result<Self, StateInitError> {
Self::new_with_debug_and_storage_validation(
config,
network,
debug_skip_format_upgrades,
read_only,
true,
true,
)
}
#[cfg(test)]
pub(crate) fn new_with_debug_without_storage_validation(
config: &Config,
network: &Network,
debug_skip_format_upgrades: bool,
read_only: bool,
) -> Result<Self, StateInitError> {
Self::new_with_debug_and_storage_validation(
config,
network,
debug_skip_format_upgrades,
read_only,
false,
true,
)
}
#[allow(clippy::unwrap_in_result)]
fn new_with_debug_and_storage_validation(
config: &Config,
network: &Network,
debug_skip_format_upgrades: bool,
read_only: bool,
validate_storage_mode: bool,
enforce_resume_guard: bool,
) -> Result<Self, StateInitError> {
if validate_storage_mode {
if let Err(error) = config.validate_storage_mode(network) {
panic!("{error}");
}
}
let db = ZakuraDb::new(
config,
STATE_DATABASE_KIND,
&state_database_format_version_in_code(),
network,
debug_skip_format_upgrades,
STATE_COLUMN_FAMILIES_IN_CODE
.iter()
.map(ToString::to_string),
read_only,
)?;
let vct = VctState::from_config(config.checkpoint_sync, config.vct_fast_sync, network);
let is_vct_sync_below_last_checkpoint = db
.vct_synced_below()
.zip(db.finalized_tip_height())
.is_some_and(|(last_checkpoint_height, tip)| tip < last_checkpoint_height);
let new_state = Self {
debug_stop_at_height: config.debug_stop_at_height.map(block::Height),
checkpoint_raw_tx_retention_start: None,
checkpoint_raw_tx_archive_backlog: Arc::new(AtomicBool::new(false)),
db,
vct: VctCommitState::new(vct, is_vct_sync_below_last_checkpoint),
};
if config.pruning_config().is_none() && new_state.db.is_pruned() {
panic!(
"this database has been pruned and cannot be opened in archive storage mode; \
configure pruned storage mode (`storage_mode.pruned`), or delete the cache \
directory and re-sync from genesis"
);
}
if enforce_resume_guard
&& new_state.vct.is_below_last_checkpoint()
&& new_state.vct.source().is_none()
{
panic!(
"this database was previously synced in verified commitment tree mode that was \
interrupted below the last checkpoint height. the fast path that supplies \
the verified roots needed to resume the VCT sync is disabled. Set \
`consensus.checkpoint_sync = true` and `consensus.vct_fast_sync = true` to \
finish the VCT sync, or delete the cache directory and re-sync from genesis"
);
}
if let Some(tip_height) = new_state.db.finalized_tip_height() {
if new_state.is_at_stop_height(tip_height) {
let debug_stop_at_height = new_state
.debug_stop_at_height
.expect("true from `is_at_stop_height` implies `debug_stop_at_height` is Some");
let tip_hash = new_state.db.finalized_tip_hash();
if tip_height > debug_stop_at_height {
tracing::error!(
?debug_stop_at_height,
?tip_height,
?tip_hash,
"previous state height is greater than the stop height",
);
}
tracing::info!(
?debug_stop_at_height,
?tip_height,
?tip_hash,
"state is already at the configured height"
);
std::mem::drop(new_state);
Self::exit_process();
}
}
Ok(new_state)
}
pub(crate) fn with_checkpoint_raw_tx_retention(
mut self,
max_checkpoint_height: block::Height,
config: &Config,
) -> Self {
self.checkpoint_raw_tx_retention_start = config.pruning_config().and_then(|pruning| {
compute_checkpoint_raw_tx_retention_start(max_checkpoint_height, pruning.tx_retention)
});
let has_archive_backlog = config.pruning_config().is_some()
&& self.checkpoint_raw_tx_retention_start.is_some_and(|start| {
let prune_from = self.db.lowest_retained_height().unwrap_or(block::Height(1));
self.db.raw_transactions_exist_in_range(prune_from, start)
});
self.checkpoint_raw_tx_archive_backlog
.store(has_archive_backlog, Ordering::Relaxed);
self
}
fn store_checkpoint_raw_transactions(&self, height: block::Height) -> bool {
height.is_min()
|| self
.checkpoint_raw_tx_retention_start
.is_none_or(|start| height >= start)
}
fn retention_plan(&self, height: block::Height, is_checkpoint: bool) -> RetentionPlan {
let Some(pruning) = self.db.config().pruning_config() else {
return RetentionPlan::Store;
};
let lowest_retained = self.db.lowest_retained_height();
if is_checkpoint && !self.store_checkpoint_raw_transactions(height) {
let skipped_until = (height + 1).expect("checkpoint block height plus one is valid");
if self
.checkpoint_raw_tx_archive_backlog
.load(Ordering::Relaxed)
{
if let Some((from, until)) = self
.db
.checkpoint_raw_transaction_prune_range(skipped_until)
{
let final_chunk =
!checkpoint_prune_range_retains_current_height(height, Some((from, until)));
return RetentionPlan::DrainBacklog {
from,
until,
final_chunk,
};
}
}
return RetentionPlan::Skip {
lowest_retained: skipped_until,
write_marker: lowest_retained < Some(skipped_until),
};
}
match ZakuraDb::prune_height_range(height, pruning.tx_retention, lowest_retained) {
Some((from, until)) => RetentionPlan::Prune { from, until },
None => RetentionPlan::Store,
}
}
#[cfg(test)]
pub(crate) fn has_checkpoint_raw_tx_archive_backlog(&self) -> bool {
self.checkpoint_raw_tx_archive_backlog
.load(Ordering::Relaxed)
}
pub fn network(&self) -> Network {
self.db.network()
}
pub fn commit_finalized(
&mut self,
ordered_block: QueuedCheckpointVerified,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
) -> Result<
(CheckpointVerifiedBlock, NoteCommitmentTrees),
(QueuedCheckpointVerified, CommitCheckpointVerifiedError),
> {
self.commit_finalized_inner(
ordered_block,
prev_note_commitment_trees,
vct_successor_witness,
None,
)
}
pub(in crate::service) fn commit_finalized_with_aux_and<C>(
&mut self,
ordered_block: QueuedCheckpointVerified,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_auxiliary_window: Option<VctAuxiliaryWindow>,
commit: C,
) -> Result<
(CheckpointVerifiedBlock, NoteCommitmentTrees),
(QueuedCheckpointVerified, CommitCheckpointVerifiedError),
>
where
C: FnOnce(
&mut ZakuraDb,
DiskWriteBatch,
VctAuthenticationProof,
) -> Result<(), CommitCheckpointVerifiedError>,
{
let vct_successor_witness = vct_auxiliary_window
.as_ref()
.and_then(|auxiliary_window| auxiliary_window.successor.clone());
self.commit_finalized_inner_with(
ordered_block,
prev_note_commitment_trees,
vct_successor_witness,
vct_auxiliary_window,
commit,
)
}
fn commit_finalized_inner(
&mut self,
ordered_block: QueuedCheckpointVerified,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
vct_auxiliary_window: Option<VctAuxiliaryWindow>,
) -> Result<
(CheckpointVerifiedBlock, NoteCommitmentTrees),
(QueuedCheckpointVerified, CommitCheckpointVerifiedError),
> {
self.commit_finalized_inner_with(
ordered_block,
prev_note_commitment_trees,
vct_successor_witness,
vct_auxiliary_window,
|db, batch, _proof| {
db.header_chain_disk_db()
.write(batch)
.expect("unexpected rocksdb error while writing block");
Ok(())
},
)
}
fn commit_finalized_inner_with<C>(
&mut self,
ordered_block: QueuedCheckpointVerified,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
vct_auxiliary_window: Option<VctAuxiliaryWindow>,
commit: C,
) -> Result<
(CheckpointVerifiedBlock, NoteCommitmentTrees),
(QueuedCheckpointVerified, CommitCheckpointVerifiedError),
>
where
C: FnOnce(
&mut ZakuraDb,
DiskWriteBatch,
VctAuthenticationProof,
) -> Result<(), CommitCheckpointVerifiedError>,
{
let (checkpoint_verified, rsp_tx) = ordered_block;
let result = self.commit_finalized_direct_with_aux(
checkpoint_verified.clone().into(),
prev_note_commitment_trees,
vct_successor_witness,
vct_auxiliary_window,
"commit checkpoint-verified request",
commit,
);
if result.is_ok() {
metrics::counter!("state.checkpoint.finalized.block.count").increment(1);
metrics::gauge!("state.checkpoint.finalized.block.height")
.set(checkpoint_verified.height.0 as f64);
metrics::gauge!("zcash.chain.verified.block.height")
.set(checkpoint_verified.height.0 as f64);
metrics::counter!("zcash.chain.verified.block.total").increment(1);
} else {
metrics::counter!("state.checkpoint.error.block.count").increment(1);
metrics::gauge!("state.checkpoint.error.block.height")
.set(checkpoint_verified.height.0 as f64);
};
match result {
Ok((hash, note_commitment_trees)) => {
let _ = rsp_tx.send(Ok(hash));
Ok((checkpoint_verified, note_commitment_trees))
}
Err(error) => Err(((checkpoint_verified, rsp_tx), error)),
}
}
#[allow(clippy::unwrap_in_result)]
pub fn commit_finalized_direct(
&mut self,
finalizable_block: FinalizableBlock,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
source: &str,
) -> Result<(block::Hash, NoteCommitmentTrees), CommitCheckpointVerifiedError> {
self.commit_finalized_direct_with(
finalizable_block,
prev_note_commitment_trees,
vct_successor_witness,
source,
|db, batch, _proof| {
db.header_chain_disk_db()
.write(batch)
.expect("unexpected rocksdb error while writing block");
Ok(())
},
)
}
#[cfg(test)]
pub(super) fn commit_finalized_direct_with_exact_aux_for_test(
&mut self,
finalizable_block: FinalizableBlock,
vct_auxiliary_window: VctAuxiliaryWindow,
source: &str,
) -> Result<(block::Hash, NoteCommitmentTrees), CommitCheckpointVerifiedError> {
let vct_successor_witness = vct_auxiliary_window.successor.clone();
self.commit_finalized_direct_with_aux(
finalizable_block,
None,
vct_successor_witness,
Some(vct_auxiliary_window),
source,
|db, batch, _proof| {
db.header_chain_disk_db()
.write(batch)
.expect("unexpected rocksdb error while writing block");
Ok(())
},
)
}
pub(in crate::service) fn commit_finalized_direct_with<C>(
&mut self,
finalizable_block: FinalizableBlock,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
source: &str,
commit: C,
) -> Result<(block::Hash, NoteCommitmentTrees), CommitCheckpointVerifiedError>
where
C: FnOnce(
&mut ZakuraDb,
DiskWriteBatch,
VctAuthenticationProof,
) -> Result<(), CommitCheckpointVerifiedError>,
{
self.commit_finalized_direct_with_aux(
finalizable_block,
prev_note_commitment_trees,
vct_successor_witness,
None,
source,
commit,
)
}
fn commit_finalized_direct_with_aux<C>(
&mut self,
finalizable_block: FinalizableBlock,
prev_note_commitment_trees: Option<NoteCommitmentTrees>,
vct_successor_witness: Option<VctSuccessorWitness>,
vct_auxiliary_window: Option<VctAuxiliaryWindow>,
source: &str,
commit: C,
) -> Result<(block::Hash, NoteCommitmentTrees), CommitCheckpointVerifiedError>
where
C: FnOnce(
&mut ZakuraDb,
DiskWriteBatch,
VctAuthenticationProof,
) -> Result<(), CommitCheckpointVerifiedError>,
{
let mut vct_authentication = VctAuthenticationProof::NotAuthenticated;
let (height, hash, finalized, prev_note_commitment_trees, retention, fast_write) =
match finalizable_block {
FinalizableBlock::Checkpoint {
checkpoint_verified,
} => {
let block = checkpoint_verified.block.clone();
let precomputed_auth_data_root = checkpoint_verified.auth_data_root;
let mut history_tree = self.db.history_tree();
let prev_note_commitment_trees = match prev_note_commitment_trees {
Some(trees) => trees,
None => self
.db
.note_commitment_trees_for_tip()
.map_err(ValidateContextError::from)?,
};
let mut note_commitment_trees = prev_note_commitment_trees.clone();
let network = self.network();
let height = checkpoint_verified.height;
let vct_last_checkpoint_height = self
.vct
.source()
.map(|v| v.vct_sync_last_checkpoint_height());
let exact_vct_roots =
vct_auxiliary_window.as_ref().and_then(|auxiliary_window| {
auxiliary_window.delivery_roots(height, block.hash())
});
let vct_roots = exact_vct_roots;
#[cfg(any(test, feature = "proptest-impl"))]
let vct_roots = match vct_auxiliary_window.as_ref() {
Some(_) => vct_roots,
None => self.vct.source().and_then(|v| {
if vct_last_checkpoint_height.is_some_and(|last_checkpoint_height| {
height > last_checkpoint_height
}) {
None
} else {
v.vct_roots_at_height(height)
}
}),
};
let mut vct_write = VctWriteData::default();
if let Some((sapling_root, orchard_root, ironwood_root)) = vct_roots {
let last_checkpoint_frontiers = self
.vct
.source()
.and_then(|v| v.final_frontiers_for_last_checkpoint(height));
let block_hash = block.hash();
let vct_successor_witness = vct_successor_witness.filter(|witness| {
let links = witness.header.previous_block_hash == block_hash;
let delivery_matches = witness.delivery.is_none_or(|delivery| {
delivery.header_hash == witness.hash
&& delivery.tree_aux.is_some_and(|auxiliary_data| {
auxiliary_data.height == witness.height
&& Some(auxiliary_data.auth_data_root)
== witness.auth_data_root
})
});
if !links || !delivery_matches {
tracing::warn!(
?height,
witness_parent = ?witness.header.previous_block_hash,
expected_parent = ?block_hash,
"VCT: ignoring an incoherent successor witness"
);
}
links && delivery_matches
});
let block_auth_data_root = (NetworkUpgrade::current(&network, height)
>= NetworkUpgrade::Nu5)
.then(|| {
precomputed_auth_data_root.unwrap_or_else(|| block.auth_data_root())
});
if let (
Some((
prevalidated_height,
prevalidated_hash,
Some(expected_auth_data_root),
)),
Some(actual_auth_data_root),
) = (self.vct.prevalidated_next(), block_auth_data_root)
{
if prevalidated_height == height
&& prevalidated_hash == block_hash
&& expected_auth_data_root != actual_auth_data_root
{
metrics::counter!("state.vct.block.auth_data_root_mismatch.count")
.increment(1);
tracing::warn!(
?height,
?block_hash,
?expected_auth_data_root,
?actual_auth_data_root,
"VCT: checkpoint body auth-data root differs from its \
authenticated header prevalidation"
);
return Err(ValidateContextError::VctBlockAuthDataRootMismatch {
height,
expected: expected_auth_data_root,
actual: actual_auth_data_root,
}
.into());
}
}
let is_prevalidated = self.vct.prevalidated_next()
== Some((height, block_hash, block_auth_data_root));
if is_prevalidated {
if let Some(v) = self.vct.source() {
v.record_prevalidated();
}
metrics::counter!("state.vct.prevalidated.block.count").increment(1);
}
let mut verification_items = vec![
commitment_aux_verify::CommitmentRootVerification::with_roots(
block.clone(),
sapling_root,
orchard_root,
ironwood_root,
precomputed_auth_data_root,
is_prevalidated,
),
];
if let Some(successor_witness) = &vct_successor_witness {
verification_items.push(
commitment_aux_verify::CommitmentRootVerification::header_only(
successor_witness.header.clone(),
successor_witness.height,
successor_witness.auth_data_root,
),
);
}
let candidate = COMMIT_COMPUTE_POOL
.install(|| {
commitment_aux_verify::verify_commitment_roots(
&network,
(*history_tree).clone(),
verification_items,
)
})
.map_err(|failure| {
self.vct.clear_prevalidated_next();
match failure {
commitment_aux_verify::CommitmentRootVerificationError::CurrentBlock {
error,
..
} => error.into(),
commitment_aux_verify::CommitmentRootVerificationError::CurrentRoots {
error,
..
} => self.vct_reject_supplied_root(
height,
error,
crate::error::VctCommitFailure::CurrentRoots,
),
commitment_aux_verify::CommitmentRootVerificationError::SuccessorBoundary {
error,
..
} => self.vct_reject_supplied_root(
height,
error,
crate::error::VctCommitFailure::SuccessorBoundary,
),
}
})?;
if let (Some(window), Some(successor)) = (
vct_auxiliary_window.as_ref(),
vct_successor_witness.as_ref(),
) {
let authenticates_history = matches!(
successor.header.commitment(&network, successor.height),
Ok(block::Commitment::ChainHistoryRoot(_)
| block::Commitment::ChainHistoryBlockTxAuthCommitment(_))
);
if authenticates_history {
if let Some(boundary_auth_data_root) = successor.auth_data_root {
vct_authentication = VctAuthenticationProof::Successor {
delivery_id: window.delivery.delivery_id,
delivery_header_hash: block_hash,
boundary_hash: successor.hash,
boundary_auth_data_root,
};
}
}
}
if let Some(successor_witness) = &vct_successor_witness {
let next_auth_data_root =
(NetworkUpgrade::current(&network, successor_witness.height)
>= NetworkUpgrade::Nu5)
.then_some(successor_witness.auth_data_root)
.flatten();
self.vct.mark_prevalidated(
successor_witness.height,
successor_witness.hash,
next_auth_data_root,
);
} else if self
.vct
.source()
.is_some_and(|v| v.vct_root_needs_successor(height, &network, true))
{
metrics::counter!("state.vct.root.await_successor.count").increment(1);
return Err(ValidateContextError::VctSuppliedRootAwaitingSuccessor {
height,
}
.into());
} else {
self.vct.clear_prevalidated_next();
}
note_commitment_trees
.update_sprout_tree(&block)
.map_err(ValidateContextError::from)?;
history_tree = Arc::new(candidate);
if let Some(v) = self.vct.source() {
v.record_fast_block();
}
metrics::counter!("state.vct.fast.block.count").increment(1);
vct_write.sync_below = vct_last_checkpoint_height;
if let Some((
sapling_frontier,
orchard_frontier,
sprout_frontier,
ironwood_frontier,
)) = last_checkpoint_frontiers
{
self.vct_verify_last_checkpoint_frontier_roots(
height,
&sapling_frontier,
&orchard_frontier,
&ironwood_frontier,
&sapling_root,
&orchard_root,
&ironwood_root,
)?;
let expected_sprout_root = sprout_frontier.root();
let actual_sprout_root = note_commitment_trees.sprout.root();
if actual_sprout_root != expected_sprout_root {
self.vct.clear_prevalidated_next();
return Err(ValidateContextError::VctSproutHandoffRootMismatch {
height,
expected: expected_sprout_root,
actual: actual_sprout_root,
}
.into());
}
note_commitment_trees = NoteCommitmentTrees {
sprout: note_commitment_trees.sprout,
sapling: sapling_frontier,
sapling_subtree: None,
orchard: orchard_frontier,
orchard_subtree: None,
ironwood: ironwood_frontier,
ironwood_subtree: None,
};
self.vct.stop_vct_sync_at_last_checkpoint();
} else {
vct_write.anchor_roots =
Some((sapling_root, orchard_root, ironwood_root));
self.vct.start_vct_sync_below_last_checkpoint();
}
} else if self.vct.is_below_last_checkpoint() {
metrics::counter!("state.vct.root.unavailable.count").increment(1);
tracing::warn!(
?height,
"VCT: no verifiable supplied root for a frozen-frontier height; \
refusing to recompute (retryable)"
);
return Err(
ValidateContextError::VctSuppliedRootUnavailable { height }.into()
);
} else {
self.vct.clear_prevalidated_next();
metrics::counter!("state.vct.legacy.block.count").increment(1);
#[cfg(feature = "commit-metrics")]
metrics::histogram!("zakura.state.write.block_tx_count")
.record(block.transactions.len() as f64);
#[cfg(feature = "commit-metrics")]
let _ckpt_compute = std::time::Instant::now();
let mut commitment_result = None;
let tree_result = COMMIT_COMPUTE_POOL.install(|| {
rayon::in_place_scope_fifo(|scope| {
scope.spawn_fifo(|_scope| {
commitment_result = Some(timed_commit_phase!(
"zakura.state.write.commitment_check.duration_seconds",
check::block_commitment_is_valid_for_chain_history(
block.clone(),
&network,
&history_tree,
precomputed_auth_data_root,
)
));
});
timed_commit_phase!(
"zakura.state.write.update_trees.duration_seconds",
note_commitment_trees.update_trees_parallel(&block)
)
})
});
tree_result.map_err(ValidateContextError::from)?;
commitment_result.expect("scope has already finished")?;
let history_tree_mut = Arc::make_mut(&mut history_tree);
let sapling_root = note_commitment_trees.sapling.root();
let orchard_root = note_commitment_trees.orchard.root();
let ironwood_root = note_commitment_trees.ironwood.root();
history_tree_mut
.push(
&network,
block.clone(),
&sapling_root,
&orchard_root,
&ironwood_root,
)
.map_err(Arc::new)
.map_err(ValidateContextError::from)?;
#[cfg(feature = "commit-metrics")]
metrics::histogram!(
"zakura.state.write.checkpoint_compute.duration_seconds"
)
.record(_ckpt_compute.elapsed().as_secs_f64());
}
let treestate = Treestate {
note_commitment_trees,
history_tree,
};
let hash = checkpoint_verified.hash;
(
height,
hash,
FinalizedBlock::from_checkpoint_verified(checkpoint_verified, treestate),
Some(prev_note_commitment_trees),
self.retention_plan(height, true),
vct_write,
)
}
FinalizableBlock::Contextual {
contextually_verified,
treestate,
} => {
let height = contextually_verified.height;
(
height,
contextually_verified.hash,
FinalizedBlock::from_contextually_verified(
contextually_verified,
*treestate,
),
prev_note_commitment_trees,
self.retention_plan(height, false),
VctWriteData::default(),
)
}
};
let committed_tip_hash = self.db.finalized_tip_hash();
let committed_tip_height = self.db.finalized_tip_height();
if self.db.is_empty() {
assert_eq!(
committed_tip_hash, finalized.block.header.previous_block_hash,
"the first block added to an empty state must be a genesis block, source: {source}",
);
assert_eq!(
block::Height(0),
height,
"cannot commit genesis: invalid height, source: {source}",
);
} else {
assert_eq!(
committed_tip_height.expect("state must have a genesis block committed") + 1,
Some(height),
"committed block height must be 1 more than the finalized tip height, source: {source}",
);
assert_eq!(
committed_tip_hash, finalized.block.header.previous_block_hash,
"committed block must be a child of the finalized tip, source: {source}",
);
}
let note_commitment_trees = finalized.treestate.note_commitment_trees.clone();
let network = self.network();
let result = self.db.write_block_with(
finalized,
prev_note_commitment_trees,
&network,
source,
retention,
fast_write,
move |db, batch| commit(db, batch, vct_authentication),
);
if result.is_ok() {
if retention.clears_archive_backlog() {
self.checkpoint_raw_tx_archive_backlog
.store(false, Ordering::Relaxed);
}
if self.is_at_stop_height(height) {
tracing::info!(
?height,
?hash,
block_source = ?source,
"stopping at configured height, flushing database to disk"
);
self.vct_log_equivalence_digest();
self.db.shutdown(true);
Self::exit_process();
}
}
result.map(|hash| (hash, note_commitment_trees))
}
pub(crate) fn vct_requires_exact_roots(&self, height: block::Height) -> bool {
self.vct
.source()
.is_some_and(|v| v.accepts_exact_roots_at(height))
}
pub(crate) fn clear_vct_prevalidated_next(&mut self) {
self.vct.clear_prevalidated_next();
}
pub(crate) fn vct_fast_needs_successor(
&self,
height: block::Height,
has_exact_roots: bool,
) -> bool {
self.vct
.source()
.is_some_and(|v| v.vct_root_needs_successor(height, &self.network(), has_exact_roots))
}
#[allow(clippy::too_many_arguments)]
fn vct_verify_last_checkpoint_frontier_roots(
&mut self,
height: block::Height,
sapling_frontier: &sapling::tree::NoteCommitmentTree,
orchard_frontier: &orchard::tree::NoteCommitmentTree,
ironwood_frontier: &ironwood::tree::NoteCommitmentTree,
sapling_root: &sapling::tree::Root,
orchard_root: &orchard::tree::Root,
ironwood_root: &ironwood::tree::Root,
) -> Result<(), CommitCheckpointVerifiedError> {
if sapling_frontier.root() != *sapling_root
|| orchard_frontier.root() != *orchard_root
|| ironwood_frontier.root() != *ironwood_root
{
self.vct.clear_prevalidated_next();
return Err(self.vct_reject_supplied_root(
height,
ValidateContextError::VctSuppliedRootUnavailable { height },
crate::error::VctCommitFailure::CurrentRoots,
));
}
Ok(())
}
fn vct_reject_supplied_root(
&self,
height: block::Height,
error: ValidateContextError,
failure: crate::error::VctCommitFailure,
) -> CommitCheckpointVerifiedError {
metrics::counter!("state.vct.root.rejected.count").increment(1);
tracing::warn!(
?height,
?error,
"VCT: supplied commitment root failed verification"
);
CommitCheckpointVerifiedError::from(ValidateContextError::VctSuppliedRootUnavailable {
height,
})
.with_vct_failure(failure)
}
#[cfg(any(test, feature = "proptest-impl"))]
pub(in crate::service::finalized_state) fn enable_vct_fast_source(
&mut self,
source: Box<dyn commitment_aux::CommitmentRootSource>,
requires_verified_successor: bool,
) {
self.vct
.install_test_source(source, requires_verified_successor);
}
#[cfg(test)]
pub(in crate::service) fn enable_vct_exact_root_source_for_test(
&mut self,
last_exact_height: block::Height,
) {
self.vct.install_test_source(
Box::new(commitment_aux::EmbeddedFrontierSource::new(
commitment_aux::FinalFrontiers {
height: last_exact_height,
sapling: Arc::new(Default::default()),
orchard: Arc::new(Default::default()),
sprout: Arc::new(Default::default()),
ironwood: Arc::new(Default::default()),
},
)),
true,
);
}
#[cfg(test)]
pub(crate) fn vct_fast_synced_below(&self) -> Option<block::Height> {
self.db.vct_synced_below()
}
#[cfg(test)]
pub(crate) fn vct_fast_count(&self) -> u64 {
self.vct.source().map(|v| v.vct_count()).unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn vct_prevalidated_count(&self) -> u64 {
self.vct
.source()
.map(|v| v.prevalidated_count())
.unwrap_or(0)
}
fn vct_log_equivalence_digest(&self) {
if std::env::var_os("VCT_DIGEST").is_none() {
return;
}
let fast_count = if let Some(v) = self.vct.source() {
v.vct_count()
} else {
0
};
let (
sapling_anchor_count,
sapling_anchor_digest,
orchard_anchor_count,
orchard_anchor_digest,
) = self.db.vct_anchor_digest();
let history_root = self.db.history_tree().hash();
tracing::info!(
sapling_anchor_count,
sapling_anchor_digest,
orchard_anchor_count,
orchard_anchor_digest,
?history_root,
vct_fast_blocks = fast_count,
"VCT-DIGEST"
);
}
fn is_at_stop_height(&self, block_height: block::Height) -> bool {
let debug_stop_at_height = match self.debug_stop_at_height {
Some(debug_stop_at_height) => debug_stop_at_height,
None => return false,
};
if block_height < debug_stop_at_height {
return false;
}
true
}
fn exit_process() -> ! {
tracing::info!("exiting Zakura");
let _ = stdout().lock().flush();
let _ = stderr().lock().flush();
std::thread::sleep(std::time::Duration::from_secs(3));
std::process::exit(0);
}
}
fn checkpoint_prune_range_retains_current_height(
height: block::Height,
checkpoint_prune_range: Option<(block::Height, block::Height)>,
) -> bool {
checkpoint_prune_range.is_some_and(|(_, prune_until)| prune_until <= height)
}
fn compute_checkpoint_raw_tx_retention_start(
max_checkpoint_height: block::Height,
tx_retention: u32,
) -> Option<block::Height> {
let max_skipped_height = max_checkpoint_height.0.checked_sub(tx_retention)?;
max_skipped_height.checked_add(1).map(block::Height)
}