From fa70f17a5e3af785fa3a4ae8e4357e07d62e291d Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 10 Jun 2026 13:50:53 +1200 Subject: [PATCH 1/4] Refactor validator server naming and modules --- bin/validator/src/commands/start.rs | 4 +- bin/validator/src/lib.rs | 2 +- bin/validator/src/server/mod.rs | 76 ++----------- .../{validate_block.rs => validator/mod.rs} | 102 ++++++++++++++---- .../src/server/{ => validator}/sign_block.rs | 4 +- .../src/server/{ => validator}/status.rs | 4 +- .../submit_proven_transaction.rs | 4 +- .../src/server/{ => validator}/tests.rs | 23 ++-- 8 files changed, 107 insertions(+), 112 deletions(-) rename bin/validator/src/server/{validate_block.rs => validator/mod.rs} (62%) rename bin/validator/src/server/{ => validator}/sign_block.rs (96%) rename bin/validator/src/server/{ => validator}/status.rs (90%) rename bin/validator/src/server/{ => validator}/submit_proven_transaction.rs (97%) rename bin/validator/src/server/{ => validator}/tests.rs (95%) diff --git a/bin/validator/src/commands/start.rs b/bin/validator/src/commands/start.rs index b246597fb0..854c521ad0 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::{ValidatorServer, ValidatorSigner}; // Starts the validator component. pub async fn start( @@ -14,7 +14,7 @@ pub async fn start( data_directory: PathBuf, sqlite_connection_pool_size: NonZeroUsize, ) -> anyhow::Result<()> { - Validator { + ValidatorServer { address, grpc_options, signer, diff --git a/bin/validator/src/lib.rs b/bin/validator/src/lib.rs index 1150ac48c1..df7aa120e6 100644 --- a/bin/validator/src/lib.rs +++ b/bin/validator/src/lib.rs @@ -3,7 +3,7 @@ mod server; mod signers; mod tx_validation; -pub use server::Validator; +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 b2d986ca7b..2167ed6b7e 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_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,24 +19,19 @@ use crate::db::{ load_chain_tip, load_with_pool_size, }; -use crate::server::validate_block::BlockValidationError; use crate::{COMPONENT, ValidatorSigner}; -#[cfg(test)] -mod tests; +mod validator; -mod sign_block; -mod status; -mod submit_proven_transaction; -mod validate_block; +use validator::Validator; -// 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). @@ -58,7 +49,7 @@ pub struct Validator { 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 @@ -100,7 +91,7 @@ impl Validator { .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( + Validator::new( self.signer, db, initial_chain_tip, @@ -116,58 +107,3 @@ impl Validator { .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 { - async fn new( - signer: ValidatorSigner, - db: Db, - 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(BlockValidationError::DatabaseError)? - .ok_or(BlockValidationError::NoChainTip)?; - let signing_key = signer.public_key(); - if &signing_key != chain_tip.validator_key() { - return Err(BlockValidationError::ValidatorKeyMismatch { - expected: chain_tip.validator_key().clone(), - actual: signing_key, - }); - } - - Ok(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/validate_block.rs b/bin/validator/src/server/validator/mod.rs similarity index 62% rename from bin/validator/src/server/validate_block.rs rename to bin/validator/src/server/validator/mod.rs index d65c0443a5..d1846de980 100644 --- a/bin/validator/src/server/validate_block.rs +++ b/bin/validator/src/server/validator/mod.rs @@ -1,20 +1,30 @@ -use miden_node_db::DatabaseError; +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, AtomicU64}; + +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 tokio::sync::Semaphore; use tracing::{Span, instrument}; -use crate::COMPONENT; -use crate::db::{find_unvalidated_transactions, load_block_header}; -use crate::server::ValidatorServer; +use crate::db::{find_unvalidated_transactions, load_block_header, load_chain_tip}; +use crate::{COMPONENT, ValidatorSigner}; + +#[cfg(test)] +mod tests; -// BLOCK VALIDATION ERROR +mod sign_block; +mod status; +mod submit_proven_transaction; + +// VALIDATOR ERROR // ================================================================================================ #[derive(thiserror::Error, Debug)] -pub enum BlockValidationError { +pub enum ValidatorError { #[error("block contains unvalidated transactions {0:?}")] UnvalidatedTransactions(Vec), #[error("failed to build block")] @@ -40,10 +50,60 @@ pub enum BlockValidationError { NoChainTip, } -// BLOCK VALIDATION -// ================================================================================================ +// VALIDATOR +// ================================================================================ + +/// The underlying implementation of the gRPC validator server. +/// +/// Implements the gRPC API for the validator. +pub(crate) struct Validator { + 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 Validator { + pub(crate) async fn new( + signer: ValidatorSigner, + db: Db, + 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(), + 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), + }) + } -impl ValidatorServer { /// 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. @@ -57,7 +117,7 @@ impl ValidatorServer { &self, proposed_block: ProposedBlock, chain_tip: BlockHeader, - ) -> Result<(Signature, BlockHeader), BlockValidationError> { + ) -> 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::>(); @@ -67,17 +127,17 @@ impl ValidatorServer { find_unvalidated_transactions(conn, &proposed_tx_ids) }) .await - .map_err(BlockValidationError::DatabaseError)?; + .map_err(ValidatorError::DatabaseError)?; // All proposed transactions must have been validated. if !unvalidated_txs.is_empty() { - return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_txs)); + return Err(ValidatorError::UnvalidatedTransactions(unvalidated_txs)); } // Build the block header. let (proposed_header, _) = proposed_block .into_header_and_body() - .map_err(BlockValidationError::BlockBuildingFailed)?; + .map_err(ValidatorError::BlockBuildingFailed)?; let span = Span::current(); span.set_attribute("block.number", proposed_header.block_num().as_u32()); @@ -88,17 +148,17 @@ impl ValidatorServer { 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)?; + 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(BlockValidationError::DatabaseError)? - .ok_or(BlockValidationError::NoPrevBlockHeader)? + .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(BlockValidationError::BlockNumberMismatch { + return Err(ValidatorError::BlockNumberMismatch { expected: expected_block_num, actual: proposed_header.block_num(), }); @@ -110,7 +170,7 @@ impl ValidatorServer { // 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); + return Err(ValidatorError::PrevBlockCommitmentMismatch); } // Check that the block's validator key is set to our own. @@ -119,7 +179,7 @@ impl ValidatorServer { // signature invalid. let signing_key = self.signer.public_key(); if &signing_key != proposed_header.validator_key() { - return Err(BlockValidationError::ValidatorKeyMismatch { + return Err(ValidatorError::ValidatorKeyMismatch { expected: proposed_header.validator_key().clone(), actual: signing_key, }); @@ -131,10 +191,10 @@ impl ValidatorServer { /// 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 { + async fn sign_header(&self, header: &BlockHeader) -> Result { self.signer .sign(header) .await - .map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string())) + .map_err(|err| ValidatorError::BlockSigningFailed(err.to_string())) } } diff --git a/bin/validator/src/server/sign_block.rs b/bin/validator/src/server/validator/sign_block.rs similarity index 96% rename from bin/validator/src/server/sign_block.rs rename to bin/validator/src/server/validator/sign_block.rs index 3ed7ca3b42..5248adfac5 100644 --- a/bin/validator/src/server/sign_block.rs +++ b/bin/validator/src/server/validator/sign_block.rs @@ -7,11 +7,11 @@ use miden_protocol::block::ProposedBlock; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_tx::utils::serde::{Deserializable, Serializable}; +use super::Validator; 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 Validator { type Input = ProposedBlock; type Output = (Signature, Word); diff --git a/bin/validator/src/server/status.rs b/bin/validator/src/server/validator/status.rs similarity index 90% rename from bin/validator/src/server/status.rs rename to bin/validator/src/server/validator/status.rs index 078b809113..637775c91d 100644 --- a/bin/validator/src/server/status.rs +++ b/bin/validator/src/server/validator/status.rs @@ -2,10 +2,10 @@ use std::sync::atomic::Ordering; use miden_node_proto::generated as grpc; -use crate::server::ValidatorServer; +use super::Validator; #[tonic::async_trait] -impl grpc::server::validator_api::Status for ValidatorServer { +impl grpc::server::validator_api::Status for Validator { type Input = (); type Output = (); diff --git a/bin/validator/src/server/submit_proven_transaction.rs b/bin/validator/src/server/validator/submit_proven_transaction.rs similarity index 97% rename from bin/validator/src/server/submit_proven_transaction.rs rename to bin/validator/src/server/validator/submit_proven_transaction.rs index d5d7d9b21d..11e108156c 100644 --- a/bin/validator/src/server/submit_proven_transaction.rs +++ b/bin/validator/src/server/validator/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::Validator; 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 Validator { type Input = Input; type Output = (); diff --git a/bin/validator/src/server/tests.rs b/bin/validator/src/server/validator/tests.rs similarity index 95% rename from bin/validator/src/server/tests.rs rename to bin/validator/src/server/validator/tests.rs index 993400dcd7..869d1538ac 100644 --- a/bin/validator/src/server/tests.rs +++ b/bin/validator/src/server/validator/tests.rs @@ -11,32 +11,31 @@ 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::{Validator, ValidatorError}; use crate::ValidatorSigner; use crate::db::{load_chain_tip, setup, upsert_block_header}; -use crate::server::validate_block::BlockValidationError; // 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: Validator, 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 [`Validator`]: 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 (db, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: ValidatorServer::new(signer, db, 0, 0, 0).await.unwrap(), + server: Validator::new(signer, db, 0, 0, 0).await.unwrap(), chain: PartialBlockchain::default(), chain_tip: genesis_header, } @@ -134,9 +133,9 @@ async fn signing_key_mismatch_rejected() { "test requires a signing key that differs from the genesis validator key", ); - let result = ValidatorServer::new(rogue_signer, db, 0, 0, 0).await; + let result = Validator::new(rogue_signer, db, 0, 0, 0).await; assert!( - matches!(result, Err(BlockValidationError::ValidatorKeyMismatch { .. })), + matches!(result, Err(ValidatorError::ValidatorKeyMismatch { .. })), "expected ValidatorKeyMismatch error", ); } @@ -410,7 +409,7 @@ async fn unknown_transactions_rejected() { 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}"), @@ -485,7 +484,7 @@ async fn validate_block_number_mismatch() { 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" ); } From e2f5f79749b74e7850c053126d7b0db683845578 Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 11 Jun 2026 19:50:37 +1200 Subject: [PATCH 2/4] Rename to validator service --- bin/validator/src/server/mod.rs | 11 +-- .../{validator => validator_service}/mod.rs | 27 ++++--- .../sign_block.rs | 18 +++-- .../status.rs | 4 +- .../submit_proven_transaction.rs | 19 +++-- .../{validator => validator_service}/tests.rs | 80 ++++++++++++++----- 6 files changed, 106 insertions(+), 53 deletions(-) rename bin/validator/src/server/{validator => validator_service}/mod.rs (92%) rename bin/validator/src/server/{validator => validator_service}/sign_block.rs (85%) rename bin/validator/src/server/{validator => validator_service}/status.rs (91%) rename bin/validator/src/server/{validator => validator_service}/submit_proven_transaction.rs (81%) rename bin/validator/src/server/{validator => validator_service}/tests.rs (91%) diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index 2167ed6b7e..e28adec7ba 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -14,16 +14,13 @@ use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; use crate::db::{ - count_signed_blocks, - count_validated_transactions, - load_chain_tip, - load_with_pool_size, + count_signed_blocks, count_validated_transactions, load_chain_tip, load_with_pool_size, }; use crate::{COMPONENT, ValidatorSigner}; -mod validator; +mod validator_service; -use validator::Validator; +use validator_service::ValidatorService; // VALIDATOR SERVER // ================================================================================ @@ -91,7 +88,7 @@ impl ValidatorServer { .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn)) .timeout(self.grpc_options.request_timeout) .add_service(api_server::ApiServer::new( - Validator::new( + ValidatorService::new( self.signer, db, initial_chain_tip, diff --git a/bin/validator/src/server/validator/mod.rs b/bin/validator/src/server/validator_service/mod.rs similarity index 92% rename from bin/validator/src/server/validator/mod.rs rename to bin/validator/src/server/validator_service/mod.rs index d1846de980..62872b0759 100644 --- a/bin/validator/src/server/validator/mod.rs +++ b/bin/validator/src/server/validator_service/mod.rs @@ -45,18 +45,21 @@ pub enum ValidatorError { #[error( "validator signing key {actual:?} does not match the block's validator key {expected:?}" )] - ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, + ValidatorKeyMismatch { + expected: PublicKey, + actual: PublicKey, + }, #[error("no chain tip exists")] NoChainTip, } -// VALIDATOR +// VALIDATOR SERVICE // ================================================================================ /// The underlying implementation of the gRPC validator server. /// /// Implements the gRPC API for the validator. -pub(crate) struct Validator { +pub(crate) struct ValidatorService { signer: ValidatorSigner, db: Arc, /// Serializes `sign_block` requests so that concurrent calls are processed sequentially, @@ -70,7 +73,7 @@ pub(crate) struct Validator { signed_blocks_count: AtomicU64, } -impl Validator { +impl ValidatorService { pub(crate) async fn new( signer: ValidatorSigner, db: Db, @@ -119,8 +122,10 @@ impl Validator { 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 proposed_tx_ids = proposed_block + .transactions() + .map(TransactionHeader::id) + .collect::>(); let unvalidated_txs = self .db .transact("find_unvalidated_transactions", move |conn| { @@ -147,10 +152,14 @@ impl Validator { // 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)?; + 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)) + .query("load_block_header", move |conn| { + load_block_header(conn, prev_block_num) + }) .await .map_err(ValidatorError::DatabaseError)? .ok_or(ValidatorError::NoPrevBlockHeader)? diff --git a/bin/validator/src/server/validator/sign_block.rs b/bin/validator/src/server/validator_service/sign_block.rs similarity index 85% rename from bin/validator/src/server/validator/sign_block.rs rename to bin/validator/src/server/validator_service/sign_block.rs index 5248adfac5..680887f099 100644 --- a/bin/validator/src/server/validator/sign_block.rs +++ b/bin/validator/src/server/validator_service/sign_block.rs @@ -7,11 +7,11 @@ use miden_protocol::block::ProposedBlock; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature; use miden_tx::utils::serde::{Deserializable, Serializable}; -use super::Validator; +use super::ValidatorService; use crate::db::{load_chain_tip, upsert_block_header}; #[tonic::async_trait] -impl grpc::server::validator_api::SignBlock for Validator { +impl grpc::server::validator_api::SignBlock for ValidatorService { type Input = ProposedBlock; type Output = (Signature, Word); @@ -26,7 +26,9 @@ impl grpc::server::validator_api::SignBlock for Validator { fn encode(output: Self::Output) -> tonic::Result { let (signature, block_commitment) = output; Ok(grpc::blockchain::SignBlockResponse { - signature: Some(grpc::blockchain::BlockSignature { signature: signature.to_bytes() }), + signature: Some(grpc::blockchain::BlockSignature { + signature: signature.to_bytes(), + }), block_commitment: Some(block_commitment.into()), }) } @@ -49,8 +51,10 @@ impl grpc::server::validator_api::SignBlock for Validator { .ok_or_else(|| tonic::Status::internal("Chain tip not found in database"))?; // Validate the block against the current chain tip. - let (signature, header) = - self.validate_block(proposed_block, 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() @@ -64,7 +68,9 @@ impl grpc::server::validator_api::SignBlock for Validator { // Persist the validated block header. let new_block_num = header.block_num().as_u32(); self.db - .transact("upsert_block_header", move |conn| upsert_block_header(conn, &header)) + .transact("upsert_block_header", move |conn| { + upsert_block_header(conn, &header) + }) .await .map_err(|err| { tonic::Status::internal(format!( diff --git a/bin/validator/src/server/validator/status.rs b/bin/validator/src/server/validator_service/status.rs similarity index 91% rename from bin/validator/src/server/validator/status.rs rename to bin/validator/src/server/validator_service/status.rs index 637775c91d..ddd03b4727 100644 --- a/bin/validator/src/server/validator/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 super::Validator; +use super::ValidatorService; #[tonic::async_trait] -impl grpc::server::validator_api::Status for Validator { +impl grpc::server::validator_api::Status for ValidatorService { type Input = (); type Output = (); diff --git a/bin/validator/src/server/validator/submit_proven_transaction.rs b/bin/validator/src/server/validator_service/submit_proven_transaction.rs similarity index 81% rename from bin/validator/src/server/validator/submit_proven_transaction.rs rename to bin/validator/src/server/validator_service/submit_proven_transaction.rs index 11e108156c..7c87c34dde 100644 --- a/bin/validator/src/server/validator/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::Validator; +use super::ValidatorService; use crate::db::insert_transaction; use crate::tx_validation::validate_transaction; #[tonic::async_trait] -impl grpc::server::validator_api::SubmitProvenTransaction for Validator { +impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService { type Input = Input; type Output = (); @@ -20,20 +20,25 @@ impl grpc::server::validator_api::SubmitProvenTransaction for Validator { tracing::Span::current().set_attribute("transaction.id", input.tx.id()); // Validate the transaction. - let tx_info = validate_transaction(input.tx, input.inputs).await.map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid transaction")) - })?; + let tx_info = validate_transaction(input.tx, input.inputs) + .await + .map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid transaction")) + })?; // Store the validated transaction. let count = self .db - .transact("insert_transaction", move |conn| insert_transaction(conn, &tx_info)) + .transact("insert_transaction", move |conn| { + insert_transaction(conn, &tx_info) + }) .await .map_err(|err| { Status::internal(err.as_report_context("Failed to insert transaction")) })?; - self.validated_transactions_count.fetch_add(count as u64, Ordering::Relaxed); + self.validated_transactions_count + .fetch_add(count as u64, Ordering::Relaxed); Ok(()) } diff --git a/bin/validator/src/server/validator/tests.rs b/bin/validator/src/server/validator_service/tests.rs similarity index 91% rename from bin/validator/src/server/validator/tests.rs rename to bin/validator/src/server/validator_service/tests.rs index 869d1538ac..1e71b1c217 100644 --- a/bin/validator/src/server/validator/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -11,7 +11,7 @@ use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::PartialBlockchain; use miden_tx::utils::serde::Serializable; -use super::{Validator, ValidatorError}; +use super::{ValidatorService, ValidatorError}; use crate::ValidatorSigner; use crate::db::{load_chain_tip, setup, upsert_block_header}; @@ -21,7 +21,7 @@ use crate::db::{load_chain_tip, setup, upsert_block_header}; /// Test harness that wraps a [`Validator`] and tracks the chain MMR state needed to construct valid /// [`ProposedBlock`]s. struct TestValidator { - server: Validator, + server: ValidatorService, chain: PartialBlockchain, chain_tip: BlockHeader, } @@ -35,7 +35,7 @@ impl TestValidator { let (db, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: Validator::new(signer, db, 0, 0, 0).await.unwrap(), + server: ValidatorService::new(signer, db, 0, 0, 0).await.unwrap(), chain: PartialBlockchain::default(), chain_tip: genesis_header, } @@ -133,7 +133,7 @@ async fn signing_key_mismatch_rejected() { "test requires a signing key that differs from the genesis validator key", ); - let result = Validator::new(rogue_signer, db, 0, 0, 0).await; + let result = ValidatorService::new(rogue_signer, db, 0, 0, 0).await; assert!( matches!(result, Err(ValidatorError::ValidatorKeyMismatch { .. })), "expected ValidatorKeyMismatch error", @@ -175,7 +175,11 @@ async fn chain_tip_plus_one_succeeds() { let proposed = tv.propose_empty_block(); let result = tv.call_sign_block(&proposed).await; - assert!(result.is_ok(), "chain tip + 1 should succeed, got: {:?}", result.err()); + assert!( + result.is_ok(), + "chain tip + 1 should succeed, got: {:?}", + result.err() + ); } /// A replacement block at the same height as the current chain tip should be accepted. @@ -211,7 +215,11 @@ async fn chain_tip_replacement_succeeds() { ); let result = tv.call_sign_block(&replacement).await; - assert!(result.is_ok(), "chain tip replacement should succeed, got: {:?}", result.err()); + assert!( + result.is_ok(), + "chain tip replacement should succeed, got: {:?}", + result.err() + ); // Verify that the chain tip in the database is now the replacement block, not the original. let new_chain_tip = tv.load_chain_tip().await; @@ -287,9 +295,16 @@ async fn commitment_mismatch_rejected() { // Build a valid ProposedBlock on a *different* genesis so its prev_block_commitment won't match // the validator's actual chain tip. let other_genesis_signer = random_secret_key(); - let other_genesis_state = - GenesisState::new(vec![], test_fee_params(), 1, 1, other_genesis_signer.public_key()); - let other_genesis_block = other_genesis_state.into_block(&other_genesis_signer).unwrap(); + let other_genesis_state = GenesisState::new( + vec![], + test_fee_params(), + 1, + 1, + other_genesis_signer.public_key(), + ); + let other_genesis_block = other_genesis_state + .into_block(&other_genesis_signer) + .unwrap(); let other_genesis_header = other_genesis_block.inner().header().clone(); let mismatched_block = empty_block(&other_genesis_header, &PartialBlockchain::default()); @@ -315,14 +330,24 @@ async fn replacement_commitment_mismatch_rejected() { // Build a replacement block at the same height but using a *different* genesis so its // prev_block_commitment won't match the validator's actual parent of the chain tip. let other_genesis_signer = random_secret_key(); - let other_genesis_state = - GenesisState::new(vec![], test_fee_params(), 1, 1, other_genesis_signer.public_key()); - let other_genesis_block = other_genesis_state.into_block(&other_genesis_signer).unwrap(); + let other_genesis_state = GenesisState::new( + vec![], + test_fee_params(), + 1, + 1, + other_genesis_signer.public_key(), + ); + let other_genesis_block = other_genesis_state + .into_block(&other_genesis_signer) + .unwrap(); let other_genesis_header = other_genesis_block.inner().header().clone(); let mismatched_replacement = empty_block(&other_genesis_header, &PartialBlockchain::default()); let result = tv.call_sign_block(&mismatched_replacement).await; - assert!(result.is_err(), "replacement with mismatched commitment should be rejected"); + assert!( + result.is_err(), + "replacement with mismatched commitment should be rejected" + ); let status = result.unwrap_err(); assert!( status.message().contains("previous block commitment"), @@ -337,10 +362,18 @@ async fn empty_block_succeeds() { let tv = TestValidator::new().await; let proposed = tv.propose_empty_block(); - assert_eq!(proposed.transactions().count(), 0, "block should have no transactions"); + assert_eq!( + proposed.transactions().count(), + 0, + "block should have no transactions" + ); let result = tv.call_sign_block(&proposed).await; - assert!(result.is_ok(), "empty block should succeed, got: {:?}", result.err()); + assert!( + result.is_ok(), + "empty block should succeed, got: {:?}", + result.err() + ); } /// A block containing transactions that were not previously validated should be rejected. @@ -352,10 +385,7 @@ async fn unknown_transactions_rejected() { use miden_protocol::block::BlockNumber; use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; use miden_protocol::transaction::{ - InputNoteCommitment, - InputNotes, - OrderedTransactionHeaders, - TransactionHeader, + InputNoteCommitment, InputNotes, OrderedTransactionHeaders, TransactionHeader, }; let tv = TestValidator::new().await; @@ -407,11 +437,14 @@ async fn unknown_transactions_rejected() { let proposed = ProposedBlock::new(block_inputs, vec![batch]).unwrap(); let result = tv.server.validate_block(proposed, genesis_header).await; - assert!(result.is_err(), "block with unknown transactions should be rejected"); + assert!( + result.is_err(), + "block with unknown transactions should be rejected" + ); match result.unwrap_err() { ValidatorError::UnvalidatedTransactions(ids) => { assert_eq!(ids, vec![tx_id], "should report the unknown transaction ID"); - }, + } other => panic!("expected UnvalidatedTransactions error, got: {other}"), } } @@ -484,7 +517,10 @@ async fn validate_block_number_mismatch() { let result = tv.server.validate_block(block_3, block_1_header).await; assert!(result.is_err()); assert!( - matches!(result.unwrap_err(), ValidatorError::BlockNumberMismatch { .. }), + matches!( + result.unwrap_err(), + ValidatorError::BlockNumberMismatch { .. } + ), "expected BlockNumberMismatch error" ); } From 82af72c8b5a3b5215587f1090162d74031c70e1b Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 11 Jun 2026 19:55:16 +1200 Subject: [PATCH 3/4] Lint --- bin/validator/src/server/mod.rs | 5 +- .../src/server/validator_service/mod.rs | 21 ++---- .../server/validator_service/sign_block.rs | 14 +--- .../submit_proven_transaction.rs | 15 ++-- .../src/server/validator_service/tests.rs | 74 +++++-------------- 5 files changed, 38 insertions(+), 91 deletions(-) diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index e28adec7ba..1a70cc7e87 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -14,7 +14,10 @@ use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; use crate::db::{ - count_signed_blocks, count_validated_transactions, load_chain_tip, load_with_pool_size, + count_signed_blocks, + count_validated_transactions, + load_chain_tip, + load_with_pool_size, }; use crate::{COMPONENT, ValidatorSigner}; diff --git a/bin/validator/src/server/validator_service/mod.rs b/bin/validator/src/server/validator_service/mod.rs index 62872b0759..a6733d3dc2 100644 --- a/bin/validator/src/server/validator_service/mod.rs +++ b/bin/validator/src/server/validator_service/mod.rs @@ -45,10 +45,7 @@ pub enum ValidatorError { #[error( "validator signing key {actual:?} does not match the block's validator key {expected:?}" )] - ValidatorKeyMismatch { - expected: PublicKey, - actual: PublicKey, - }, + ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, #[error("no chain tip exists")] NoChainTip, } @@ -122,10 +119,8 @@ impl ValidatorService { 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 proposed_tx_ids = + proposed_block.transactions().map(TransactionHeader::id).collect::>(); let unvalidated_txs = self .db .transact("find_unvalidated_transactions", move |conn| { @@ -152,14 +147,10 @@ impl ValidatorService { // 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)?; + 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) - }) + .query("load_block_header", move |conn| load_block_header(conn, prev_block_num)) .await .map_err(ValidatorError::DatabaseError)? .ok_or(ValidatorError::NoPrevBlockHeader)? diff --git a/bin/validator/src/server/validator_service/sign_block.rs b/bin/validator/src/server/validator_service/sign_block.rs index 680887f099..e75c6fcfdb 100644 --- a/bin/validator/src/server/validator_service/sign_block.rs +++ b/bin/validator/src/server/validator_service/sign_block.rs @@ -26,9 +26,7 @@ impl grpc::server::validator_api::SignBlock for ValidatorService { fn encode(output: Self::Output) -> tonic::Result { let (signature, block_commitment) = output; Ok(grpc::blockchain::SignBlockResponse { - signature: Some(grpc::blockchain::BlockSignature { - signature: signature.to_bytes(), - }), + signature: Some(grpc::blockchain::BlockSignature { signature: signature.to_bytes() }), block_commitment: Some(block_commitment.into()), }) } @@ -51,10 +49,8 @@ impl grpc::server::validator_api::SignBlock for ValidatorService { .ok_or_else(|| tonic::Status::internal("Chain tip not found in database"))?; // Validate the block against the current chain tip. - let (signature, header) = self - .validate_block(proposed_block, 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() @@ -68,9 +64,7 @@ impl grpc::server::validator_api::SignBlock for ValidatorService { // Persist the validated block header. let new_block_num = header.block_num().as_u32(); self.db - .transact("upsert_block_header", move |conn| { - upsert_block_header(conn, &header) - }) + .transact("upsert_block_header", move |conn| upsert_block_header(conn, &header)) .await .map_err(|err| { tonic::Status::internal(format!( diff --git a/bin/validator/src/server/validator_service/submit_proven_transaction.rs b/bin/validator/src/server/validator_service/submit_proven_transaction.rs index 7c87c34dde..d981fd90a6 100644 --- a/bin/validator/src/server/validator_service/submit_proven_transaction.rs +++ b/bin/validator/src/server/validator_service/submit_proven_transaction.rs @@ -20,25 +20,20 @@ impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService { tracing::Span::current().set_attribute("transaction.id", input.tx.id()); // Validate the transaction. - let tx_info = validate_transaction(input.tx, input.inputs) - .await - .map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid transaction")) - })?; + let tx_info = validate_transaction(input.tx, input.inputs).await.map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid transaction")) + })?; // Store the validated transaction. let count = self .db - .transact("insert_transaction", move |conn| { - insert_transaction(conn, &tx_info) - }) + .transact("insert_transaction", move |conn| insert_transaction(conn, &tx_info)) .await .map_err(|err| { Status::internal(err.as_report_context("Failed to insert transaction")) })?; - self.validated_transactions_count - .fetch_add(count as u64, Ordering::Relaxed); + self.validated_transactions_count.fetch_add(count as u64, Ordering::Relaxed); Ok(()) } diff --git a/bin/validator/src/server/validator_service/tests.rs b/bin/validator/src/server/validator_service/tests.rs index 1e71b1c217..3090b87f5a 100644 --- a/bin/validator/src/server/validator_service/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -11,7 +11,7 @@ use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::PartialBlockchain; use miden_tx::utils::serde::Serializable; -use super::{ValidatorService, ValidatorError}; +use super::{ValidatorError, ValidatorService}; use crate::ValidatorSigner; use crate::db::{load_chain_tip, setup, upsert_block_header}; @@ -175,11 +175,7 @@ async fn chain_tip_plus_one_succeeds() { let proposed = tv.propose_empty_block(); let result = tv.call_sign_block(&proposed).await; - assert!( - result.is_ok(), - "chain tip + 1 should succeed, got: {:?}", - result.err() - ); + assert!(result.is_ok(), "chain tip + 1 should succeed, got: {:?}", result.err()); } /// A replacement block at the same height as the current chain tip should be accepted. @@ -215,11 +211,7 @@ async fn chain_tip_replacement_succeeds() { ); let result = tv.call_sign_block(&replacement).await; - assert!( - result.is_ok(), - "chain tip replacement should succeed, got: {:?}", - result.err() - ); + assert!(result.is_ok(), "chain tip replacement should succeed, got: {:?}", result.err()); // Verify that the chain tip in the database is now the replacement block, not the original. let new_chain_tip = tv.load_chain_tip().await; @@ -295,16 +287,9 @@ async fn commitment_mismatch_rejected() { // Build a valid ProposedBlock on a *different* genesis so its prev_block_commitment won't match // the validator's actual chain tip. let other_genesis_signer = random_secret_key(); - let other_genesis_state = GenesisState::new( - vec![], - test_fee_params(), - 1, - 1, - other_genesis_signer.public_key(), - ); - let other_genesis_block = other_genesis_state - .into_block(&other_genesis_signer) - .unwrap(); + let other_genesis_state = + GenesisState::new(vec![], test_fee_params(), 1, 1, other_genesis_signer.public_key()); + let other_genesis_block = other_genesis_state.into_block(&other_genesis_signer).unwrap(); let other_genesis_header = other_genesis_block.inner().header().clone(); let mismatched_block = empty_block(&other_genesis_header, &PartialBlockchain::default()); @@ -330,24 +315,14 @@ async fn replacement_commitment_mismatch_rejected() { // Build a replacement block at the same height but using a *different* genesis so its // prev_block_commitment won't match the validator's actual parent of the chain tip. let other_genesis_signer = random_secret_key(); - let other_genesis_state = GenesisState::new( - vec![], - test_fee_params(), - 1, - 1, - other_genesis_signer.public_key(), - ); - let other_genesis_block = other_genesis_state - .into_block(&other_genesis_signer) - .unwrap(); + let other_genesis_state = + GenesisState::new(vec![], test_fee_params(), 1, 1, other_genesis_signer.public_key()); + let other_genesis_block = other_genesis_state.into_block(&other_genesis_signer).unwrap(); let other_genesis_header = other_genesis_block.inner().header().clone(); let mismatched_replacement = empty_block(&other_genesis_header, &PartialBlockchain::default()); let result = tv.call_sign_block(&mismatched_replacement).await; - assert!( - result.is_err(), - "replacement with mismatched commitment should be rejected" - ); + assert!(result.is_err(), "replacement with mismatched commitment should be rejected"); let status = result.unwrap_err(); assert!( status.message().contains("previous block commitment"), @@ -362,18 +337,10 @@ async fn empty_block_succeeds() { let tv = TestValidator::new().await; let proposed = tv.propose_empty_block(); - assert_eq!( - proposed.transactions().count(), - 0, - "block should have no transactions" - ); + assert_eq!(proposed.transactions().count(), 0, "block should have no transactions"); let result = tv.call_sign_block(&proposed).await; - assert!( - result.is_ok(), - "empty block should succeed, got: {:?}", - result.err() - ); + assert!(result.is_ok(), "empty block should succeed, got: {:?}", result.err()); } /// A block containing transactions that were not previously validated should be rejected. @@ -385,7 +352,10 @@ async fn unknown_transactions_rejected() { use miden_protocol::block::BlockNumber; use miden_protocol::testing::account_id::ACCOUNT_ID_SENDER; use miden_protocol::transaction::{ - InputNoteCommitment, InputNotes, OrderedTransactionHeaders, TransactionHeader, + InputNoteCommitment, + InputNotes, + OrderedTransactionHeaders, + TransactionHeader, }; let tv = TestValidator::new().await; @@ -437,14 +407,11 @@ async fn unknown_transactions_rejected() { let proposed = ProposedBlock::new(block_inputs, vec![batch]).unwrap(); let result = tv.server.validate_block(proposed, genesis_header).await; - assert!( - result.is_err(), - "block with unknown transactions should be rejected" - ); + assert!(result.is_err(), "block with unknown transactions should be rejected"); match result.unwrap_err() { ValidatorError::UnvalidatedTransactions(ids) => { assert_eq!(ids, vec![tx_id], "should report the unknown transaction ID"); - } + }, other => panic!("expected UnvalidatedTransactions error, got: {other}"), } } @@ -517,10 +484,7 @@ async fn validate_block_number_mismatch() { let result = tv.server.validate_block(block_3, block_1_header).await; assert!(result.is_err()); assert!( - matches!( - result.unwrap_err(), - ValidatorError::BlockNumberMismatch { .. } - ), + matches!(result.unwrap_err(), ValidatorError::BlockNumberMismatch { .. }), "expected BlockNumberMismatch error" ); } From 572f28837b47ef8b3914db9b6b0f3c3451e86c3e Mon Sep 17 00:00:00 2001 From: Serge Radinovich <47865535+sergerad@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:29:01 +1200 Subject: [PATCH 4/4] Validator block backup (#2232) --- bin/validator/src/commands/bootstrap.rs | 3 +++ bin/validator/src/server/mod.rs | 6 +++++ .../src/server/validator_service/mod.rs | 22 ++++++++++++++++--- .../src/server/validator_service/tests.rs | 16 ++++++++------ crates/store/src/lib.rs | 1 + 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index 4f9ca3a076..0d40ab19ed 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 1a70cc7e87..4ab190950d 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 a6733d3dc2..4741eccc37 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 3090b87f5a..6eeb1dce19 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 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;