diff --git a/bin/validator/src/block_validation/mod.rs b/bin/validator/src/block_validation/mod.rs deleted file mode 100644 index 24d5356258..0000000000 --- a/bin/validator/src/block_validation/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use miden_node_db::{DatabaseError, Db}; -use miden_node_utils::tracing::OpenTelemetrySpanExt; -use miden_protocol::block::{BlockHeader, BlockNumber, ProposedBlock}; -use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature}; -use miden_protocol::errors::ProposedBlockError; -use miden_protocol::transaction::{TransactionHeader, TransactionId}; -use tracing::{Span, instrument}; - -use crate::db::{find_unvalidated_transactions, load_block_header}; -use crate::{COMPONENT, ValidatorSigner}; - -// BLOCK VALIDATION ERROR -// ================================================================================================ - -#[derive(thiserror::Error, Debug)] -pub enum BlockValidationError { - #[error("block contains unvalidated transactions {0:?}")] - UnvalidatedTransactions(Vec), - #[error("failed to build block")] - BlockBuildingFailed(#[source] ProposedBlockError), - #[error("failed to sign block: {0}")] - BlockSigningFailed(String), - #[error("failed to select transactions")] - DatabaseError(#[source] DatabaseError), - #[error("block number mismatch: expected {expected}, got {actual}")] - BlockNumberMismatch { - expected: BlockNumber, - actual: BlockNumber, - }, - #[error("previous block commitment does not match chain tip")] - PrevBlockCommitmentMismatch, - #[error("no previous block header available for chain tip overwrite")] - NoPrevBlockHeader, - #[error( - "validator signing key {actual:?} does not match the block's validator key {expected:?}" - )] - ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, -} - -// BLOCK VALIDATION -// ================================================================================================ - -/// Validates a proposed block by checking: -/// 1. All transactions have been previously validated by this validator. -/// 2. The block header can be successfully built from the proposed block. -/// 3. The block is either: a. The valid next block in the chain (sequential block number, matching -/// previous block commitment), or b. A replacement block at the same height as the current chain -/// tip, validated against the previous block header. -/// -/// On success, returns the signature and the validated block header. -#[instrument(target = COMPONENT, skip_all, err, fields(tip.number = chain_tip.block_num().as_u32()))] -pub async fn validate_block( - proposed_block: ProposedBlock, - signer: &ValidatorSigner, - db: &Db, - chain_tip: BlockHeader, -) -> Result<(Signature, BlockHeader), BlockValidationError> { - // Search for any proposed transactions that have not previously been validated. - let proposed_tx_ids = - proposed_block.transactions().map(TransactionHeader::id).collect::>(); - let unvalidated_txs = db - .transact("find_unvalidated_transactions", move |conn| { - find_unvalidated_transactions(conn, &proposed_tx_ids) - }) - .await - .map_err(BlockValidationError::DatabaseError)?; - - // All proposed transactions must have been validated. - if !unvalidated_txs.is_empty() { - return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_txs)); - } - - // Build the block header. - let (proposed_header, _) = proposed_block - .into_header_and_body() - .map_err(BlockValidationError::BlockBuildingFailed)?; - - let span = Span::current(); - span.set_attribute("block.number", proposed_header.block_num().as_u32()); - span.set_attribute("block.commitment", proposed_header.commitment()); - - // If the proposed block has the same block number as the current chain tip, this is a - // replacement block. Validate it against the previous block header. - let prev = if proposed_header.block_num() == chain_tip.block_num() { - // The genesis block cannot be replaced (genesis block has no parent). - let prev_block_num = - chain_tip.block_num().parent().ok_or(BlockValidationError::NoPrevBlockHeader)?; - db.query("load_block_header", move |conn| load_block_header(conn, prev_block_num)) - .await - .map_err(BlockValidationError::DatabaseError)? - .ok_or(BlockValidationError::NoPrevBlockHeader)? - } else { - // Proposed block is a new block. Block number must be sequential. - let expected_block_num = chain_tip.block_num().child(); - if proposed_header.block_num() != expected_block_num { - return Err(BlockValidationError::BlockNumberMismatch { - expected: expected_block_num, - actual: proposed_header.block_num(), - }); - } - // Current chain tip is the parent of the proposed block. - chain_tip - }; - - // The proposed block's parent must match the block that the Validator has determined is its - // parent (either chain tip or parent of chain tip). - if proposed_header.prev_block_commitment() != prev.commitment() { - return Err(BlockValidationError::PrevBlockCommitmentMismatch); - } - - // Check that the block's validator key is set to our own. - // - // Otherwise we could be signing a block for a different key, making the - // signature invalid. - let signing_key = signer.public_key(); - if &signing_key != proposed_header.validator_key() { - return Err(BlockValidationError::ValidatorKeyMismatch { - expected: proposed_header.validator_key().clone(), - actual: signing_key, - }); - } - - let signature = sign_header(signer, &proposed_header).await?; - Ok((signature, proposed_header)) -} - -/// Signs a block header using the validator's signer. -#[instrument(target = COMPONENT, name = "sign_block", skip_all, err, fields(block.number = header.block_num().as_u32()))] -async fn sign_header( - signer: &ValidatorSigner, - header: &BlockHeader, -) -> Result { - signer - .sign(header) - .await - .map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string())) -} diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index 4f9ca3a076..30d3484f0a 100644 --- a/bin/validator/src/commands/bootstrap.rs +++ b/bin/validator/src/commands/bootstrap.rs @@ -2,15 +2,14 @@ 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; -use miden_validator::ValidatorSigner; +use miden_validator::{DataDirectory, ValidatorSigner}; use super::ValidatorKey; -const GENESIS_BLOCK_FILENAME: &str = "genesis.dat"; - // Bootstraps the validator component. pub async fn bootstrap( genesis_block_directory: &Path, @@ -34,15 +33,13 @@ pub async fn bootstrap( } let signer = validator_key.into_signer().await?; - build_and_write_genesis( - config, - signer, - accounts_directory, - genesis_block_directory, - data_directory, - sqlite_connection_pool_size, + let dirs = DataDirectory::load_bootstrap( + genesis_block_directory.to_path_buf(), + accounts_directory.to_path_buf(), + data_directory.to_path_buf(), ) - .await + .context("failed to load bootstrap directories")?; + build_and_write_genesis(config, signer, dirs, sqlite_connection_pool_size).await } /// Builds the genesis state, writes account secret files, signs the genesis block, writes it to @@ -50,16 +47,14 @@ pub async fn bootstrap( async fn build_and_write_genesis( config: GenesisConfig, signer: ValidatorSigner, - accounts_directory: &Path, - genesis_block_directory: &Path, - data_directory: &Path, + dirs: DataDirectory, sqlite_connection_pool_size: NonZeroUsize, ) -> anyhow::Result<()> { let (genesis_state, secrets) = config.into_state(signer.public_key())?; for item in secrets.as_account_files(&genesis_state) { let AccountFileWithName { account_file, name } = item?; - let account_path = accounts_directory.join(name); + let account_path = dirs.accounts_dir().expect("bootstrap directories").join(name); // Do not override existing keys. fs_err::OpenOptions::new() .create_new(true) @@ -81,12 +76,14 @@ async fn build_and_write_genesis( .context("failed to build the genesis block")?; let block_bytes = genesis_block.inner().to_bytes(); - 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")?; + fs_err::write(dirs.genesis_block_path().expect("bootstrap directories"), block_bytes) + .context("failed to write genesis block")?; + + let _ = BlockStore::bootstrap(dirs.block_store_dir(), &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"), + dirs.database_path(), sqlite_connection_pool_size, ) .await diff --git a/bin/validator/src/commands/mod.rs b/bin/validator/src/commands/mod.rs index 147d08a9bb..52119853c1 100644 --- a/bin/validator/src/commands/mod.rs +++ b/bin/validator/src/commands/mod.rs @@ -10,7 +10,7 @@ use miden_node_utils::clap::GrpcOptionsInternal; use miden_node_utils::logging::OpenTelemetry; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::utils::serde::Deserializable; -use miden_validator::ValidatorSigner; +use miden_validator::{DataDirectory, ValidatorSigner}; const ENV_DATA_DIRECTORY: &str = "MIDEN_VALIDATOR_DATA_DIRECTORY"; const ENV_LISTEN: &str = "MIDEN_VALIDATOR_LISTEN"; @@ -140,7 +140,9 @@ impl ValidatorCommand { .await }, Self::Migrate { data_directory } => { - miden_validator::db::migrate(data_directory.join("validator.sqlite3")) + let data_dir = DataDirectory::load_server(data_directory) + .context("failed to load validator data directory")?; + miden_validator::db::migrate(data_dir.database_path()) .context("failed to apply validator database migrations")?; Ok(()) }, diff --git a/bin/validator/src/commands/start.rs b/bin/validator/src/commands/start.rs index b246597fb0..9a6c5f3b3e 100644 --- a/bin/validator/src/commands/start.rs +++ b/bin/validator/src/commands/start.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use anyhow::Context; use miden_node_utils::clap::GrpcOptionsInternal; -use miden_validator::{Validator, ValidatorSigner}; +use miden_validator::{DataDirectory, ValidatorServer, ValidatorSigner}; // Starts the validator component. pub async fn start( @@ -14,7 +14,9 @@ pub async fn start( data_directory: PathBuf, sqlite_connection_pool_size: NonZeroUsize, ) -> anyhow::Result<()> { - Validator { + let data_directory = DataDirectory::load_server(data_directory) + .context("failed to load validator data directory")?; + ValidatorServer { address, grpc_options, signer, diff --git a/bin/validator/src/data_directory.rs b/bin/validator/src/data_directory.rs new file mode 100644 index 0000000000..4713766e83 --- /dev/null +++ b/bin/validator/src/data_directory.rs @@ -0,0 +1,76 @@ +use std::ops::Not; +use std::path::{Path, PathBuf}; + +/// Represents the validator's directories and their content paths. +/// +/// Used to keep our filepath assumptions in one location. +#[derive(Clone)] +pub enum DataDirectory { + /// Runtime mode: just the data directory. + Server { data: PathBuf }, + /// Bootstrap mode: genesis block, accounts, and data directories. + Bootstrap { + genesis_block: PathBuf, + accounts: PathBuf, + data: PathBuf, + }, +} + +impl DataDirectory { + /// Loads a data directory for use by the `start` and `migrate` commands. + pub fn load_server(data: PathBuf) -> std::io::Result { + verify_is_dir(&data)?; + Ok(Self::Server { data }) + } + + /// Loads a data directory for use by the `bootstrap` command. + pub fn load_bootstrap( + genesis_block: PathBuf, + accounts: PathBuf, + data: PathBuf, + ) -> std::io::Result { + for dir in [&genesis_block, &accounts, &data] { + verify_is_dir(dir)?; + } + Ok(Self::Bootstrap { genesis_block, accounts, data }) + } + + pub fn database_path(&self) -> PathBuf { + self.data().join("validator.sqlite3") + } + + pub fn block_store_dir(&self) -> PathBuf { + self.data().join("blocks") + } + + pub fn genesis_block_path(&self) -> Option { + match self { + Self::Bootstrap { genesis_block, .. } => Some(genesis_block.join("genesis.dat")), + Self::Server { .. } => None, + } + } + + pub fn accounts_dir(&self) -> Option<&Path> { + match self { + Self::Bootstrap { accounts, .. } => Some(accounts), + Self::Server { .. } => None, + } + } + + pub fn display(&self) -> std::path::Display<'_> { + self.data().display() + } + + fn data(&self) -> &PathBuf { + match self { + Self::Server { data } | Self::Bootstrap { data, .. } => data, + } + } +} + +fn verify_is_dir(path: &PathBuf) -> std::io::Result<()> { + if fs_err::metadata(path)?.is_dir().not() { + return Err(std::io::ErrorKind::NotConnected.into()); + } + Ok(()) +} diff --git a/bin/validator/src/lib.rs b/bin/validator/src/lib.rs index e556cc59c4..aef40d7546 100644 --- a/bin/validator/src/lib.rs +++ b/bin/validator/src/lib.rs @@ -1,10 +1,11 @@ -mod block_validation; +pub mod data_directory; pub mod db; mod server; mod signers; mod tx_validation; -pub use server::Validator; +pub use data_directory::DataDirectory; +pub use server::ValidatorServer; pub use signers::{KmsSigner, ValidatorSigner}; // CONSTANTS diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index a692ffeb42..946f018267 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -1,18 +1,14 @@ use std::net::SocketAddr; use std::num::NonZeroUsize; -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::{AtomicU32, AtomicU64}; use anyhow::Context; -use miden_node_db::Db; 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; use tokio::net::TcpListener; -use tokio::sync::Semaphore; use tokio_stream::wrappers::TcpListenerStream; use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; @@ -23,22 +19,19 @@ use crate::db::{ load_chain_tip, load_with_pool_size, }; -use crate::{COMPONENT, ValidatorSigner}; +use crate::{COMPONENT, DataDirectory, ValidatorSigner}; -#[cfg(test)] -mod tests; +mod validator_service; -mod sign_block; -mod status; -mod submit_proven_transaction; +use validator_service::ValidatorService; -// VALIDATOR +// VALIDATOR SERVER // ================================================================================ /// The handle into running the gRPC validator server. /// /// Facilitates the running of the gRPC server which implements the validator API. -pub struct Validator { +pub struct ValidatorServer { /// The address of the validator component. pub address: SocketAddr, /// gRPC server options for internal services (timeouts, connection caps). @@ -50,13 +43,13 @@ pub struct Validator { pub signer: ValidatorSigner, /// The data directory for the validator component's database files. - pub data_directory: PathBuf, + pub data_directory: DataDirectory, /// Maximum number of SQLite connections in the validator database connection pool. pub sqlite_connection_pool_size: NonZeroUsize, } -impl Validator { +impl ValidatorServer { /// Serves the validator RPC API. /// /// Executes in place (i.e. not spawned) and will run indefinitely until a fatal error is @@ -66,12 +59,16 @@ impl Validator { // Initialize database connection. let db = load_with_pool_size( - self.data_directory.join("validator.sqlite3"), + self.data_directory.database_path(), self.sqlite_connection_pool_size, ) .await .context("failed to initialize validator database")?; + // Initialize block store. + let block_store = BlockStore::load(self.data_directory.block_store_dir()) + .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| { @@ -97,55 +94,21 @@ impl Validator { .layer(CatchPanicLayer::custom(catch_panic_layer_fn)) .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn)) .timeout(self.grpc_options.request_timeout) - .add_service(api_server::ApiServer::new(ValidatorServer::new( - self.signer, - db, - initial_chain_tip, - initial_tx_count, - initial_block_count, - ))) + .add_service(api_server::ApiServer::new( + ValidatorService::new( + self.signer, + db, + block_store, + initial_chain_tip, + initial_tx_count, + initial_block_count, + ) + .await + .context("failed to initialize validator server")?, + )) .add_service(reflection_service) .serve_with_incoming(TcpListenerStream::new(listener)) .await .context("failed to serve validator API") } } - -// VALIDATOR SERVER -// ================================================================================ - -/// The underlying implementation of the gRPC validator server. -/// -/// Implements the gRPC API for the validator. -struct ValidatorServer { - signer: ValidatorSigner, - db: Arc, - /// Serializes `sign_block` requests so that concurrent calls are processed sequentially, - /// ensuring consistent chain tip reads and preventing race conditions. - sign_block_semaphore: Semaphore, - /// In-memory chain tip, updated atomically after each signed block. - chain_tip: AtomicU32, - /// In-memory count of validated transactions, incremented after each new insert. - validated_transactions_count: AtomicU64, - /// In-memory count of signed blocks, incremented after each signed block. - signed_blocks_count: AtomicU64, -} - -impl ValidatorServer { - fn new( - signer: ValidatorSigner, - db: Db, - initial_chain_tip: u32, - initial_tx_count: u64, - initial_block_count: u64, - ) -> Self { - Self { - signer, - db: db.into(), - sign_block_semaphore: Semaphore::new(1), - chain_tip: AtomicU32::new(initial_chain_tip), - validated_transactions_count: AtomicU64::new(initial_tx_count), - signed_blocks_count: AtomicU64::new(initial_block_count), - } - } -} diff --git a/bin/validator/src/server/validator_service/mod.rs b/bin/validator/src/server/validator_service/mod.rs new file mode 100644 index 0000000000..4741eccc37 --- /dev/null +++ b/bin/validator/src/server/validator_service/mod.rs @@ -0,0 +1,216 @@ +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, 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; +use tracing::{Span, instrument}; + +use crate::db::{find_unvalidated_transactions, load_block_header, load_chain_tip}; +use crate::{COMPONENT, ValidatorSigner}; + +#[cfg(test)] +mod tests; + +mod sign_block; +mod status; +mod submit_proven_transaction; + +// VALIDATOR ERROR +// ================================================================================================ + +#[derive(thiserror::Error, Debug)] +pub enum ValidatorError { + #[error("block contains unvalidated transactions {0:?}")] + UnvalidatedTransactions(Vec), + #[error("failed to build block")] + BlockBuildingFailed(#[source] ProposedBlockError), + #[error("failed to sign block: {0}")] + BlockSigningFailed(String), + #[error("failed to select transactions")] + DatabaseError(#[source] DatabaseError), + #[error("block number mismatch: expected {expected}, got {actual}")] + BlockNumberMismatch { + expected: BlockNumber, + actual: BlockNumber, + }, + #[error("previous block commitment does not match chain tip")] + PrevBlockCommitmentMismatch, + #[error("no previous block header available for chain tip overwrite")] + NoPrevBlockHeader, + #[error( + "validator signing key {actual:?} does not match the block's validator key {expected:?}" + )] + ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, + #[error("no chain tip exists")] + NoChainTip, + #[error("failed to backup block")] + BlockBackupFailed(#[source] std::io::Error), +} + +// VALIDATOR SERVICE +// ================================================================================ + +/// The underlying implementation of the gRPC validator server. +/// +/// Implements the gRPC API for the validator. +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, + /// In-memory chain tip, updated atomically after each signed block. + chain_tip: AtomicU32, + /// In-memory count of validated transactions, incremented after each new insert. + validated_transactions_count: AtomicU64, + /// In-memory count of signed blocks, incremented after each signed block. + signed_blocks_count: AtomicU64, +} + +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, + ) -> Result { + // The validator key is fixed at genesis and carried forward unchanged by every block, so + // the signing key must match the chain's validator key for this validator's lifetime. + // Reject a misconfigured key here. + let chain_tip = db + .query("load_chain_tip", load_chain_tip) + .await + .map_err(ValidatorError::DatabaseError)? + .ok_or(ValidatorError::NoChainTip)?; + let signing_key = signer.public_key(); + if &signing_key != chain_tip.validator_key() { + return Err(ValidatorError::ValidatorKeyMismatch { + expected: chain_tip.validator_key().clone(), + actual: signing_key, + }); + } + + 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), + signed_blocks_count: AtomicU64::new(initial_block_count), + }) + } + + /// Validates a proposed block by checking: + /// 1. All transactions have been previously validated by this validator. + /// 2. The block header can be successfully built from the proposed block. + /// 3. The block is either: a. The valid next block in the chain (sequential block number, matching + /// previous block commitment), or b. A replacement block at the same height as the current chain + /// tip, validated against the previous block header. + /// + /// On success, returns the signature and the validated block header. + #[instrument(target = COMPONENT, skip_all, err, fields(tip.number = chain_tip.block_num().as_u32()))] + pub async fn validate_block( + &self, + proposed_block: ProposedBlock, + chain_tip: BlockHeader, + ) -> Result<(Signature, BlockHeader), ValidatorError> { + // Search for any proposed transactions that have not previously been validated. + let proposed_tx_ids = + proposed_block.transactions().map(TransactionHeader::id).collect::>(); + let unvalidated_txs = self + .db + .transact("find_unvalidated_transactions", move |conn| { + find_unvalidated_transactions(conn, &proposed_tx_ids) + }) + .await + .map_err(ValidatorError::DatabaseError)?; + + // All proposed transactions must have been validated. + if !unvalidated_txs.is_empty() { + return Err(ValidatorError::UnvalidatedTransactions(unvalidated_txs)); + } + + // Build the block header. + let (proposed_header, proposed_body) = proposed_block + .into_header_and_body() + .map_err(ValidatorError::BlockBuildingFailed)?; + + let span = Span::current(); + span.set_attribute("block.number", proposed_header.block_num().as_u32()); + span.set_attribute("block.commitment", proposed_header.commitment()); + + // If the proposed block has the same block number as the current chain tip, this is a + // replacement block. Validate it against the previous block header. + let prev = if proposed_header.block_num() == chain_tip.block_num() { + // The genesis block cannot be replaced (genesis block has no parent). + let prev_block_num = + chain_tip.block_num().parent().ok_or(ValidatorError::NoPrevBlockHeader)?; + self.db + .query("load_block_header", move |conn| load_block_header(conn, prev_block_num)) + .await + .map_err(ValidatorError::DatabaseError)? + .ok_or(ValidatorError::NoPrevBlockHeader)? + } else { + // Proposed block is a new block. Block number must be sequential. + let expected_block_num = chain_tip.block_num().child(); + if proposed_header.block_num() != expected_block_num { + return Err(ValidatorError::BlockNumberMismatch { + expected: expected_block_num, + actual: proposed_header.block_num(), + }); + } + // Current chain tip is the parent of the proposed block. + chain_tip + }; + + // The proposed block's parent must match the block that the Validator has determined is its + // parent (either chain tip or parent of chain tip). + if proposed_header.prev_block_commitment() != prev.commitment() { + return Err(ValidatorError::PrevBlockCommitmentMismatch); + } + + // Check that the block's validator key is set to our own. + // + // Otherwise we could be signing a block for a different key, making the + // signature invalid. + let signing_key = self.signer.public_key(); + if &signing_key != proposed_header.validator_key() { + return Err(ValidatorError::ValidatorKeyMismatch { + expected: proposed_header.validator_key().clone(), + actual: signing_key, + }); + } + + let signature = self.sign_header(&proposed_header).await?; + + // 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. + #[instrument(target = COMPONENT, name = "sign_block", skip_all, err, fields(block.number = header.block_num().as_u32()))] + async fn sign_header(&self, header: &BlockHeader) -> Result { + self.signer + .sign(header) + .await + .map_err(|err| ValidatorError::BlockSigningFailed(err.to_string())) + } +} diff --git a/bin/validator/src/server/sign_block.rs b/bin/validator/src/server/validator_service/sign_block.rs similarity index 91% rename from bin/validator/src/server/sign_block.rs rename to bin/validator/src/server/validator_service/sign_block.rs index d2477449d1..e75c6fcfdb 100644 --- a/bin/validator/src/server/sign_block.rs +++ b/bin/validator/src/server/validator_service/sign_block.rs @@ -7,12 +7,11 @@ use miden_protocol::block::ProposedBlock; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_tx::utils::serde::{Deserializable, Serializable}; -use crate::block_validation::validate_block; +use super::ValidatorService; use crate::db::{load_chain_tip, upsert_block_header}; -use crate::server::ValidatorServer; #[tonic::async_trait] -impl grpc::server::validator_api::SignBlock for ValidatorServer { +impl grpc::server::validator_api::SignBlock for ValidatorService { type Input = ProposedBlock; type Output = (Signature, Word); @@ -50,9 +49,8 @@ impl grpc::server::validator_api::SignBlock for ValidatorServer { .ok_or_else(|| tonic::Status::internal("Chain tip not found in database"))?; // Validate the block against the current chain tip. - let (signature, header) = validate_block(proposed_block, &self.signer, &self.db, chain_tip) - .await - .map_err(|err| { + let (signature, header) = + self.validate_block(proposed_block, chain_tip).await.map_err(|err| { tonic::Status::invalid_argument(format!( "Failed to validate block: {}", err.as_report() diff --git a/bin/validator/src/server/status.rs b/bin/validator/src/server/validator_service/status.rs similarity index 91% rename from bin/validator/src/server/status.rs rename to bin/validator/src/server/validator_service/status.rs index 26f4e54da3..9ceb92a4d9 100644 --- a/bin/validator/src/server/status.rs +++ b/bin/validator/src/server/validator_service/status.rs @@ -2,10 +2,10 @@ use std::sync::atomic::Ordering; use miden_node_proto::generated as grpc; -use crate::server::ValidatorServer; +use super::ValidatorService; #[tonic::async_trait] -impl grpc::server::validator_api::Status for ValidatorServer { +impl grpc::server::validator_api::Status for ValidatorService { type Input = (); type Output = (); diff --git a/bin/validator/src/server/submit_proven_transaction.rs b/bin/validator/src/server/validator_service/submit_proven_transaction.rs similarity index 97% rename from bin/validator/src/server/submit_proven_transaction.rs rename to bin/validator/src/server/validator_service/submit_proven_transaction.rs index d5d7d9b21d..d981fd90a6 100644 --- a/bin/validator/src/server/submit_proven_transaction.rs +++ b/bin/validator/src/server/validator_service/submit_proven_transaction.rs @@ -7,12 +7,12 @@ use miden_protocol::transaction::{ProvenTransaction, TransactionInputs}; use miden_tx::utils::serde::Deserializable; use tonic::Status; +use super::ValidatorService; use crate::db::insert_transaction; -use crate::server::ValidatorServer; use crate::tx_validation::validate_transaction; #[tonic::async_trait] -impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorServer { +impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService { type Input = Input; type Output = (); diff --git a/bin/validator/src/server/tests.rs b/bin/validator/src/server/validator_service/tests.rs similarity index 86% rename from bin/validator/src/server/tests.rs rename to bin/validator/src/server/validator_service/tests.rs index 3f47223810..ff7079c934 100644 --- a/bin/validator/src/server/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -2,52 +2,40 @@ 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}; +use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::PartialBlockchain; use miden_tx::utils::serde::Serializable; -use super::ValidatorServer; +use super::{ValidatorError, ValidatorService}; use crate::ValidatorSigner; use crate::db::{load_chain_tip, setup, upsert_block_header}; // TEST HELPERS // ================================================================================================ -/// Test harness that wraps a [`ValidatorServer`] and tracks the chain MMR state needed to construct -/// valid [`ProposedBlock`]s. +/// Test harness that wraps a [`Validator`] and tracks the chain MMR state needed to construct valid +/// [`ProposedBlock`]s. struct TestValidator { - server: ValidatorServer, + server: ValidatorService, chain: PartialBlockchain, chain_tip: BlockHeader, } impl TestValidator { - /// Creates a correctly configured [`ValidatorServer`]: the validator signs blocks with the same - /// key that is designated as the `validator_key` in the genesis block. + /// Creates a correctly configured [`ValidatorService`]: the validator signs blocks with the + /// same key that is designated as the `validator_key` in the genesis block. async fn new() -> Self { let key = random_secret_key(); let signer = ValidatorSigner::new_local(key.clone()); - - 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(); - - db.transact("upsert_genesis", { - let h = genesis_header.clone(); - move |conn| upsert_block_header(conn, &h) - }) - .await - .unwrap(); + let (db, block_store, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: ValidatorServer::new(signer, db, 0, 0, 0), + server: ValidatorService::new(signer, db, block_store, 0, 0, 0).await.unwrap(), chain: PartialBlockchain::default(), chain_tip: genesis_header, } @@ -69,16 +57,6 @@ impl TestValidator { api_server::Api::sign_block(&self.server, request).await } - /// Returns a reference to the validator's database. - fn db(&self) -> &miden_node_db::Db { - &self.server.db - } - - /// Returns a reference to the validator's signer. - fn signer(&self) -> &ValidatorSigner { - &self.server.signer - } - /// Loads the current chain tip from the validator's database. async fn load_chain_tip(&self) -> BlockHeader { self.server @@ -102,6 +80,28 @@ 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, 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(); + move |conn| upsert_block_header(conn, &h) + }) + .await + .unwrap(); + + (db, block_store, genesis_header) +} + /// Builds an empty [`ProposedBlock`] that extends the given parent block header using the provided /// partial blockchain state. fn empty_block(parent_header: &BlockHeader, chain: &PartialBlockchain) -> ProposedBlock { @@ -119,32 +119,26 @@ fn empty_block(parent_header: &BlockHeader, chain: &PartialBlockchain) -> Propos // ================================================================================================ /// A validator whose signing key does not match the `validator_key` designated by the chain -/// (carried forward from genesis) must reject block signing with a clear error, rather than -/// silently handing back a signature that the block producer cannot verify. +/// (carried forward from genesis) must fail to start, rather than coming up and silently producing +/// signatures that the block producer cannot verify. #[tokio::test] async fn signing_key_mismatch_rejected() { - use crate::block_validation::{BlockValidationError, validate_block}; - - let tv = TestValidator::new().await; + // Seed a database whose genesis designates `genesis_key` as the validator key. + let genesis_key = random_secret_key(); + let (db, block_store, genesis_header) = setup_db_with_genesis(&genesis_key).await; - // A valid block 1 built on the real genesis. Its `validator_key` is carried forward from - // genesis. - let proposed = tv.propose_empty_block(); - let chain_tip = tv.chain_tip.clone(); - - // Validate with a different signer than the one designated in genesis, modelling a validator - // started with the wrong key. + // Start a validator with a different key, modelling a validator configured with the wrong key. let rogue_signer = ValidatorSigner::new_local(random_secret_key()); assert_ne!( rogue_signer.public_key(), - *chain_tip.validator_key(), + *genesis_header.validator_key(), "test requires a signing key that differs from the genesis validator key", ); - let err = validate_block(proposed, &rogue_signer, tv.db(), chain_tip).await.unwrap_err(); + let result = ValidatorService::new(rogue_signer, db, block_store, 0, 0, 0).await; assert!( - matches!(err, BlockValidationError::ValidatorKeyMismatch { .. }), - "expected ValidatorKeyMismatch, got: {err}", + matches!(result, Err(ValidatorError::ValidatorKeyMismatch { .. })), + "expected ValidatorKeyMismatch error", ); } @@ -366,8 +360,6 @@ async fn unknown_transactions_rejected() { TransactionHeader, }; - use crate::block_validation::{BlockValidationError, validate_block}; - let tv = TestValidator::new().await; let genesis_header = tv.chain_tip.clone(); @@ -416,10 +408,10 @@ async fn unknown_transactions_rejected() { ); let proposed = ProposedBlock::new(block_inputs, vec![batch]).unwrap(); - let result = validate_block(proposed, tv.signer(), tv.db(), genesis_header).await; + let result = tv.server.validate_block(proposed, genesis_header).await; assert!(result.is_err(), "block with unknown transactions should be rejected"); match result.unwrap_err() { - BlockValidationError::UnvalidatedTransactions(ids) => { + ValidatorError::UnvalidatedTransactions(ids) => { assert_eq!(ids, vec![tx_id], "should report the unknown transaction ID"); }, other => panic!("expected UnvalidatedTransactions error, got: {other}"), @@ -477,8 +469,6 @@ async fn new_block_after_replacement_with_stale_commitment_rejected() { /// Verify that `validate_block` rejects blocks with a non-sequential block number. #[tokio::test] async fn validate_block_number_mismatch() { - use crate::block_validation::{BlockValidationError, validate_block}; - let mut tv = TestValidator::new().await; // Advance to block 1. @@ -493,10 +483,10 @@ async fn validate_block_number_mismatch() { chain.add_block(&block_1_header, false); let block_3 = empty_block(&block_2_header, &chain); - let result = validate_block(block_3, tv.signer(), tv.db(), block_1_header).await; + let result = tv.server.validate_block(block_3, block_1_header).await; assert!(result.is_err()); assert!( - matches!(result.unwrap_err(), BlockValidationError::BlockNumberMismatch { .. }), + matches!(result.unwrap_err(), ValidatorError::BlockNumberMismatch { .. }), "expected BlockNumberMismatch error" ); } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 3b36b68584..dd0bc66ba7 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;