zakura 1.7.0

Zakura, an independent, consensus-compatible implementation of a Zcash node
//! `rollback-state` subcommand - roll back Zebra's finalized chain state on disk.

use std::path::PathBuf;

use clap::{Parser, ValueEnum};
use eyre::Result;

use zakura_chain::{block, parameters::Network};
use zakura_state::{RollbackFinalizedStateOptions, RollbackFinalizedStateSummary, StorageMode};

use crate::config::ZakuradConfig;

/// Storage modes accepted by the rollback command.
#[derive(Clone, Copy, Debug, ValueEnum)]
enum RollbackStorageMode {
    Archive,
    Pruned,
}

/// Roll back Zebra's finalized state to a previous height
#[derive(Debug, Default, Parser)]
pub struct RollbackStateCmd {
    /// Roll back finalized state to this block height.
    #[clap(long, help = "finalized block height to keep as the new tip")]
    height: u32,

    /// Path to Zebra's cached state.
    ///
    /// The `zakurad rollback-state` subcommand defaults to `state.cache_dir` from the loaded
    /// `zakura.toml`, while the standalone `zakura-rollback-state` binary defaults to
    /// `zakura_state::Config::default()`. Pass `--cache-dir` to be explicit and avoid rolling back
    /// the wrong (or an empty) state directory.
    #[clap(long, short, help = "path to directory with the Zakura chain state")]
    cache_dir: Option<PathBuf>,

    /// Override the loaded state storage mode.
    ///
    /// The standalone binary defaults to archive mode. Use `--storage-mode pruned` for a
    /// database without transparent indexes. When omitted, `zakurad rollback-state` retains
    /// the mode and pruning settings from the loaded configuration.
    #[clap(long, value_enum)]
    storage_mode: Option<RollbackStorageMode>,

    /// The network of the chain to roll back.
    #[clap(
        long,
        short,
        required = true,
        help = "the network of the chain to load"
    )]
    network: Network,

    /// Write removed finalized blocks to the non-finalized backup cache.
    #[clap(long, help = "keep rolled-back blocks for non-finalized restore")]
    keep_rolled_back_blocks: bool,

    /// Preview the rollback plan without mutating finalized state.
    #[clap(long, help = "show the rollback plan without changing finalized state")]
    dry_run: bool,
}

impl RollbackStateCmd {
    /// `rollback-state` sub-command entrypoint.
    ///
    /// Returns an error after logging it, so `zakurad` exits non-zero like the standalone
    /// `zakura-rollback-state` binary, and scripts can detect the failure.
    pub(crate) fn run(&self, config: &ZakuradConfig) -> Result<()> {
        self.run_with_config(config.mirrored_state_config(), config.consensus.clone())
            .inspect_err(|error| tracing::error!("Failed to roll back finalized state: {error}"))
    }

    /// Runs rollback using `state_config` as the base state configuration.
    ///
    /// `consensus_config` supplies `checkpoint_sync`, which sets the max checkpoint height used to
    /// guard `--keep-rolled-back-blocks`: blocks kept below it would be discarded on the next start.
    /// Its checkpoint-sync settings are also mirrored into `state_config`, as `start` does.
    /// Storage mode is preserved unless explicitly overridden by `--storage-mode`.
    #[allow(clippy::print_stdout)]
    pub fn run_with_config(
        &self,
        mut state_config: zakura_state::Config,
        consensus_config: zakura_consensus::config::Config,
    ) -> Result<()> {
        if let Some(cache_dir) = self.cache_dir.clone() {
            state_config.cache_dir = cache_dir;
        }

        if let Some(storage_mode) = self.storage_mode {
            state_config.storage_mode = match storage_mode {
                RollbackStorageMode::Archive => StorageMode::Archive,
                RollbackStorageMode::Pruned => {
                    StorageMode::Pruned(state_config.pruning_config().cloned().unwrap_or_default())
                }
            };
        }

        state_config.checkpoint_sync = consensus_config.checkpoint_sync;
        state_config.vct_fast_sync = consensus_config.vct_fast_sync_enabled();

        let (_, max_checkpoint_height) =
            zakura_consensus::router::init_checkpoint_list(consensus_config, &self.network);

        let options = RollbackFinalizedStateOptions {
            target_height: block::Height(self.height),
            keep_rolled_back_blocks: self.keep_rolled_back_blocks,
            max_checkpoint_height: Some(max_checkpoint_height),
        };

        if self.dry_run {
            let preview = zakura_state::preview_rollback_finalized_state(
                state_config,
                &self.network,
                options,
            )?;
            print_summary("rollback plan", &preview);
            return Ok(());
        }

        let summary = zakura_state::rollback_finalized_state(state_config, &self.network, options)?;
        print_summary("rollback complete", &summary);

        Ok(())
    }
}

#[allow(clippy::print_stdout)]
fn print_summary(label: &str, summary: &RollbackFinalizedStateSummary) {
    println!("{label}:");
    println!(
        "  old finalized tip: height {}, hash {}",
        summary.old_tip.0 .0, summary.old_tip.1
    );
    println!(
        "  new finalized tip: height {}, hash {}",
        summary.new_tip.0 .0, summary.new_tip.1
    );
    println!("  rolled-back blocks: {}", summary.rolled_back_count);

    if let Some(backup) = &summary.backup {
        println!(
            "  non-finalized backup: {} blocks in {}",
            backup.block_count,
            backup.path.display()
        );
    }
}