diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index 4f9ca3a07..0d40ab19e 100644 --- a/bin/validator/src/commands/bootstrap.rs +++ b/bin/validator/src/commands/bootstrap.rs @@ -2,6 +2,7 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use anyhow::Context; +use miden_node_store::BlockStore; use miden_node_store::genesis::config::{AccountFileWithName, GenesisConfig}; use miden_node_utils::fs::ensure_empty_directory; use miden_protocol::utils::serde::Serializable; @@ -84,6 +85,8 @@ async fn build_and_write_genesis( let genesis_block_path = genesis_block_directory.join(GENESIS_BLOCK_FILENAME); fs_err::write(&genesis_block_path, block_bytes).context("failed to write genesis block")?; + let _ = BlockStore::bootstrap(data_directory.to_path_buf().join("blocks"), &genesis_block)?; + let (genesis_header, ..) = genesis_block.into_inner().into_parts(); let db = miden_validator::db::setup_with_pool_size( data_directory.join("validator.sqlite3"), diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index 1a70cc7e8..4ab190950 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use anyhow::Context; use miden_node_proto::generated::validator::api_server; use miden_node_proto_build::validator_api_descriptor; +use miden_node_store::BlockStore; use miden_node_utils::clap::GrpcOptionsInternal; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::grpc::grpc_trace_fn; @@ -65,6 +66,10 @@ impl ValidatorServer { .await .context("failed to initialize validator database")?; + // Initialize block store. + let block_store = BlockStore::load(self.data_directory.join("blocks").clone()) + .context("failed to load block store")?; + // Load initial metrics from the database for the in-memory counters. let (initial_chain_tip, initial_tx_count, initial_block_count) = db .query("load_initial_metrics", |conn| { @@ -94,6 +99,7 @@ impl ValidatorServer { ValidatorService::new( self.signer, db, + block_store, initial_chain_tip, initial_tx_count, initial_block_count, diff --git a/bin/validator/src/server/validator_service/mod.rs b/bin/validator/src/server/validator_service/mod.rs index a6733d3dc..4741eccc3 100644 --- a/bin/validator/src/server/validator_service/mod.rs +++ b/bin/validator/src/server/validator_service/mod.rs @@ -2,9 +2,11 @@ use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64}; use miden_node_db::{DatabaseError, Db}; +use miden_node_store::BlockStore; use miden_node_utils::tracing::OpenTelemetrySpanExt; -use miden_protocol::block::{BlockHeader, BlockNumber, ProposedBlock}; +use miden_protocol::block::{BlockHeader, BlockNumber, ProposedBlock, SignedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature}; +use miden_protocol::crypto::utils::Serializable; use miden_protocol::errors::ProposedBlockError; use miden_protocol::transaction::{TransactionHeader, TransactionId}; use tokio::sync::Semaphore; @@ -48,6 +50,8 @@ pub enum ValidatorError { ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, #[error("no chain tip exists")] NoChainTip, + #[error("failed to backup block")] + BlockBackupFailed(#[source] std::io::Error), } // VALIDATOR SERVICE @@ -59,6 +63,7 @@ pub enum ValidatorError { pub(crate) struct ValidatorService { signer: ValidatorSigner, db: Arc, + block_store: BlockStore, /// Serializes `sign_block` requests so that concurrent calls are processed sequentially, /// ensuring consistent chain tip reads and preventing race conditions. sign_block_semaphore: Semaphore, @@ -74,6 +79,7 @@ impl ValidatorService { pub(crate) async fn new( signer: ValidatorSigner, db: Db, + block_store: BlockStore, initial_chain_tip: u32, initial_tx_count: u64, initial_block_count: u64, @@ -97,6 +103,7 @@ impl ValidatorService { Ok(Self { signer, db: db.into(), + block_store, sign_block_semaphore: Semaphore::new(1), chain_tip: AtomicU32::new(initial_chain_tip), validated_transactions_count: AtomicU64::new(initial_tx_count), @@ -135,7 +142,7 @@ impl ValidatorService { } // Build the block header. - let (proposed_header, _) = proposed_block + let (proposed_header, proposed_body) = proposed_block .into_header_and_body() .map_err(ValidatorError::BlockBuildingFailed)?; @@ -186,7 +193,16 @@ impl ValidatorService { } let signature = self.sign_header(&proposed_header).await?; - Ok((signature, proposed_header)) + + // Back up the signed block to disk. + let signed_block = SignedBlock::new_unchecked(proposed_header, proposed_body, signature); + self.block_store + .save_block(signed_block.header().block_num(), &signed_block.to_bytes()) + .await + .map_err(ValidatorError::BlockBackupFailed)?; + + let (header, _, signature) = signed_block.into_parts(); + Ok((signature, header)) } /// Signs a block header using the validator's signer. diff --git a/bin/validator/src/server/validator_service/tests.rs b/bin/validator/src/server/validator_service/tests.rs index 3090b87f5..6eeb1dce1 100644 --- a/bin/validator/src/server/validator_service/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; -use miden_node_store::GenesisState; +use miden_node_store::{BlockStore, GenesisState}; use miden_node_utils::fee::test_fee_params; use miden_protocol::Word; use miden_protocol::block::{BlockHeader, BlockInputs, ProposedBlock}; @@ -32,10 +32,10 @@ impl TestValidator { async fn new() -> Self { let key = random_secret_key(); let signer = ValidatorSigner::new_local(key.clone()); - let (db, genesis_header) = setup_db_with_genesis(&key).await; + let (db, block_store, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: ValidatorService::new(signer, db, 0, 0, 0).await.unwrap(), + server: ValidatorService::new(signer, db, block_store, 0, 0, 0).await.unwrap(), chain: PartialBlockchain::default(), chain_tip: genesis_header, } @@ -82,13 +82,15 @@ impl TestValidator { /// Creates a validator database seeded with a genesis block whose `validator_key` is the public key /// of `key`. Returns the database handle and the genesis block header. -async fn setup_db_with_genesis(key: &SigningKey) -> (miden_node_db::Db, BlockHeader) { +async fn setup_db_with_genesis(key: &SigningKey) -> (miden_node_db::Db, BlockStore, BlockHeader) { let genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 0, key.public_key()); let genesis_block = genesis_state.into_block(key).unwrap(); let genesis_header = genesis_block.inner().header().clone(); let dir = tempfile::tempdir().unwrap(); let db = setup(dir.path().join("validator.sqlite3")).await.unwrap(); + let block_store = + BlockStore::bootstrap(dir.path().join("blocks").clone(), &genesis_block).unwrap(); db.transact("upsert_genesis", { let h = genesis_header.clone(); @@ -97,7 +99,7 @@ async fn setup_db_with_genesis(key: &SigningKey) -> (miden_node_db::Db, BlockHea .await .unwrap(); - (db, genesis_header) + (db, block_store, genesis_header) } /// Builds an empty [`ProposedBlock`] that extends the given parent block header using the provided @@ -123,7 +125,7 @@ fn empty_block(parent_header: &BlockHeader, chain: &PartialBlockchain) -> Propos async fn signing_key_mismatch_rejected() { // Seed a database whose genesis designates `genesis_key` as the validator key. let genesis_key = random_secret_key(); - let (db, genesis_header) = setup_db_with_genesis(&genesis_key).await; + let (db, block_store, genesis_header) = setup_db_with_genesis(&genesis_key).await; // Start a validator with a different key, modelling a validator configured with the wrong key. let rogue_signer = ValidatorSigner::new_local(random_secret_key()); @@ -133,7 +135,7 @@ async fn signing_key_mismatch_rejected() { "test requires a signing key that differs from the genesis validator key", ); - let result = ValidatorService::new(rogue_signer, db, 0, 0, 0).await; + let result = ValidatorService::new(rogue_signer, db, block_store, 0, 0, 0).await; assert!( matches!(result, Err(ValidatorError::ValidatorKeyMismatch { .. })), "expected ValidatorKeyMismatch error", diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 3b36b6858..dd0bc66ba 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -11,6 +11,7 @@ pub mod state; #[cfg(feature = "rocksdb")] pub use accounts::PersistentAccountTree; pub use accounts::{AccountTreeWithHistory, HistoricalError, InMemoryAccountTree}; +pub use blocks::BlockStore; pub use data_directory::DataDirectory; pub use db::models::conv::SqlTypeConvert; pub use db::models::queries::StorageMapValuesPage;