From 907a4822d802720c5a607bd23c8ed788f5698e80 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 10 Jun 2026 11:54:39 +1200 Subject: [PATCH 1/5] Move validation to member fn --- bin/validator/src/block_validation/mod.rs | 137 ------------------- bin/validator/src/lib.rs | 1 - bin/validator/src/server/mod.rs | 6 +- bin/validator/src/server/sign_block.rs | 12 +- bin/validator/src/server/tests.rs | 82 ++++++++---- bin/validator/src/server/validate_block.rs | 149 +++++++++++++++++++++ 6 files changed, 218 insertions(+), 169 deletions(-) delete mode 100644 bin/validator/src/block_validation/mod.rs create mode 100644 bin/validator/src/server/validate_block.rs 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/lib.rs b/bin/validator/src/lib.rs index e556cc59c4..1150ac48c1 100644 --- a/bin/validator/src/lib.rs +++ b/bin/validator/src/lib.rs @@ -1,4 +1,3 @@ -mod block_validation; pub mod db; mod server; mod signers; diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index a692ffeb42..7973a6766c 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -18,10 +18,7 @@ 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}; @@ -31,6 +28,7 @@ mod tests; mod sign_block; mod status; mod submit_proven_transaction; +mod validate_block; // VALIDATOR // ================================================================================ diff --git a/bin/validator/src/server/sign_block.rs b/bin/validator/src/server/sign_block.rs index d2477449d1..56f1fb961a 100644 --- a/bin/validator/src/server/sign_block.rs +++ b/bin/validator/src/server/sign_block.rs @@ -7,7 +7,6 @@ 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 crate::db::{load_chain_tip, upsert_block_header}; use crate::server::ValidatorServer; @@ -27,7 +26,9 @@ impl grpc::server::validator_api::SignBlock for ValidatorServer { 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()), }) } @@ -50,7 +51,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) + let (signature, header) = self + .validate_block(proposed_block, chain_tip) .await .map_err(|err| { tonic::Status::invalid_argument(format!( @@ -66,7 +68,9 @@ impl grpc::server::validator_api::SignBlock for ValidatorServer { // 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/tests.rs b/bin/validator/src/server/tests.rs index 3f47223810..abd6b475ed 100644 --- a/bin/validator/src/server/tests.rs +++ b/bin/validator/src/server/tests.rs @@ -123,8 +123,6 @@ fn empty_block(parent_header: &BlockHeader, chain: &PartialBlockchain) -> Propos /// silently handing back a signature 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; // A valid block 1 built on the real genesis. Its `validator_key` is carried forward from @@ -141,7 +139,11 @@ async fn signing_key_mismatch_rejected() { "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 err = tv + .server + .validate_block(proposed, &rogue_signer, tv.db(), chain_tip) + .await + .unwrap_err(); assert!( matches!(err, BlockValidationError::ValidatorKeyMismatch { .. }), "expected ValidatorKeyMismatch, got: {err}", @@ -183,7 +185,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. @@ -219,7 +225,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; @@ -295,9 +305,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()); @@ -323,14 +340,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"), @@ -345,10 +372,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. @@ -360,10 +395,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, }; use crate::block_validation::{BlockValidationError, validate_block}; @@ -417,11 +449,14 @@ 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; - 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() { BlockValidationError::UnvalidatedTransactions(ids) => { assert_eq!(ids, vec![tx_id], "should report the unknown transaction ID"); - }, + } other => panic!("expected UnvalidatedTransactions error, got: {other}"), } } @@ -477,8 +512,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. @@ -496,7 +529,10 @@ async fn validate_block_number_mismatch() { let result = validate_block(block_3, tv.signer(), tv.db(), block_1_header).await; assert!(result.is_err()); assert!( - matches!(result.unwrap_err(), BlockValidationError::BlockNumberMismatch { .. }), + matches!( + result.unwrap_err(), + BlockValidationError::BlockNumberMismatch { .. } + ), "expected BlockNumberMismatch error" ); } diff --git a/bin/validator/src/server/validate_block.rs b/bin/validator/src/server/validate_block.rs new file mode 100644 index 0000000000..1116360a89 --- /dev/null +++ b/bin/validator/src/server/validate_block.rs @@ -0,0 +1,149 @@ +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::COMPONENT; +use crate::db::{find_unvalidated_transactions, load_block_header}; +use crate::server::ValidatorServer; + +// 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 +// ================================================================================================ + +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. + /// 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), 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 = self + .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)?; + self.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 = self.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 = self.sign_header(&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(&self, header: &BlockHeader) -> Result { + self.signer + .sign(header) + .await + .map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string())) + } +} From 4eba8d539c566bebe60c6af02c3ce178c6590eaa Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 10 Jun 2026 13:30:06 +1200 Subject: [PATCH 2/5] Move validate block logic and test key on start --- bin/validator/src/server/mod.rs | 48 +++++-- bin/validator/src/server/sign_block.rs | 14 +- bin/validator/src/server/tests.rs | 155 +++++++-------------- bin/validator/src/server/validate_block.rs | 29 ++-- 4 files changed, 104 insertions(+), 142 deletions(-) diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index 7973a6766c..b2d986ca7b 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -18,8 +18,12 @@ 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::server::validate_block::BlockValidationError; use crate::{COMPONENT, ValidatorSigner}; #[cfg(test)] @@ -95,13 +99,17 @@ 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( + ValidatorServer::new( + self.signer, + db, + 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 @@ -130,20 +138,36 @@ struct ValidatorServer { } impl ValidatorServer { - fn new( + async fn new( signer: ValidatorSigner, db: Db, initial_chain_tip: u32, initial_tx_count: u64, initial_block_count: u64, - ) -> Self { - Self { + ) -> 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/sign_block.rs b/bin/validator/src/server/sign_block.rs index 56f1fb961a..3ed7ca3b42 100644 --- a/bin/validator/src/server/sign_block.rs +++ b/bin/validator/src/server/sign_block.rs @@ -26,9 +26,7 @@ impl grpc::server::validator_api::SignBlock for ValidatorServer { 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 ValidatorServer { .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 ValidatorServer { // 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/tests.rs b/bin/validator/src/server/tests.rs index abd6b475ed..993400dcd7 100644 --- a/bin/validator/src/server/tests.rs +++ b/bin/validator/src/server/tests.rs @@ -6,6 +6,7 @@ use miden_node_store::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; @@ -13,6 +14,7 @@ use miden_tx::utils::serde::Serializable; use super::ValidatorServer; use crate::ValidatorSigner; use crate::db::{load_chain_tip, setup, upsert_block_header}; +use crate::server::validate_block::BlockValidationError; // TEST HELPERS // ================================================================================================ @@ -31,23 +33,10 @@ impl TestValidator { 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, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: ValidatorServer::new(signer, db, 0, 0, 0), + server: ValidatorServer::new(signer, db, 0, 0, 0).await.unwrap(), chain: PartialBlockchain::default(), chain_tip: genesis_header, } @@ -69,16 +58,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 +81,26 @@ 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) { + 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(); + + (db, 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,34 +118,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() { - let tv = TestValidator::new().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(); + // 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; - // 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 = tv - .server - .validate_block(proposed, &rogue_signer, tv.db(), chain_tip) - .await - .unwrap_err(); + let result = ValidatorServer::new(rogue_signer, db, 0, 0, 0).await; assert!( - matches!(err, BlockValidationError::ValidatorKeyMismatch { .. }), - "expected ValidatorKeyMismatch, got: {err}", + matches!(result, Err(BlockValidationError::ValidatorKeyMismatch { .. })), + "expected ValidatorKeyMismatch error", ); } @@ -185,11 +176,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. @@ -225,11 +212,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; @@ -305,16 +288,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()); @@ -340,24 +316,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"), @@ -372,18 +338,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. @@ -395,11 +353,12 @@ 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, }; - use crate::block_validation::{BlockValidationError, validate_block}; - let tv = TestValidator::new().await; let genesis_header = tv.chain_tip.clone(); @@ -448,15 +407,12 @@ 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; - assert!( - result.is_err(), - "block with unknown transactions should be 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) => { assert_eq!(ids, vec![tx_id], "should report the unknown transaction ID"); - } + }, other => panic!("expected UnvalidatedTransactions error, got: {other}"), } } @@ -526,13 +482,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(), BlockValidationError::BlockNumberMismatch { .. }), "expected BlockNumberMismatch error" ); } diff --git a/bin/validator/src/server/validate_block.rs b/bin/validator/src/server/validate_block.rs index 1116360a89..d65c0443a5 100644 --- a/bin/validator/src/server/validate_block.rs +++ b/bin/validator/src/server/validate_block.rs @@ -1,4 +1,4 @@ -use miden_node_db::{DatabaseError, Db}; +use miden_node_db::DatabaseError; use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_protocol::block::{BlockHeader, BlockNumber, ProposedBlock}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature}; @@ -35,10 +35,9 @@ pub enum BlockValidationError { #[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, } // BLOCK VALIDATION @@ -60,10 +59,8 @@ impl ValidatorServer { 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 proposed_tx_ids = + proposed_block.transactions().map(TransactionHeader::id).collect::>(); let unvalidated_txs = self .db .transact("find_unvalidated_transactions", move |conn| { @@ -74,9 +71,7 @@ impl ValidatorServer { // All proposed transactions must have been validated. if !unvalidated_txs.is_empty() { - return Err(BlockValidationError::UnvalidatedTransactions( - unvalidated_txs, - )); + return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_txs)); } // Build the block header. @@ -92,14 +87,10 @@ impl ValidatorServer { // 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)?; + let prev_block_num = + chain_tip.block_num().parent().ok_or(BlockValidationError::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(BlockValidationError::DatabaseError)? .ok_or(BlockValidationError::NoPrevBlockHeader)? From eeeafd8eb3a24d399edfcb4e7d13b31c3f343bd4 Mon Sep 17 00:00:00 2001 From: Serge Radinovich <47865535+sergerad@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:29:16 +1200 Subject: [PATCH 3/5] Refactor validator server naming and modules (#2231) --- bin/validator/src/commands/bootstrap.rs | 3 + bin/validator/src/commands/start.rs | 4 +- bin/validator/src/lib.rs | 2 +- bin/validator/src/server/mod.rs | 82 ++---------- .../mod.rs} | 124 ++++++++++++++---- .../{ => validator_service}/sign_block.rs | 4 +- .../server/{ => validator_service}/status.rs | 4 +- .../submit_proven_transaction.rs | 4 +- .../server/{ => validator_service}/tests.rs | 35 ++--- crates/store/src/lib.rs | 1 + 10 files changed, 143 insertions(+), 120 deletions(-) rename bin/validator/src/server/{validate_block.rs => validator_service/mod.rs} (56%) rename bin/validator/src/server/{ => validator_service}/sign_block.rs (96%) rename bin/validator/src/server/{ => validator_service}/status.rs (90%) rename bin/validator/src/server/{ => validator_service}/submit_proven_transaction.rs (97%) rename bin/validator/src/server/{ => validator_service}/tests.rs (93%) 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/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..4ab190950d 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -1,18 +1,15 @@ 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,24 +20,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_service; -mod sign_block; -mod status; -mod submit_proven_transaction; -mod validate_block; +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). @@ -58,7 +50,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 @@ -74,6 +66,10 @@ impl Validator { .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| { @@ -100,9 +96,10 @@ 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( + ValidatorService::new( self.signer, db, + block_store, initial_chain_tip, initial_tx_count, initial_block_count, @@ -116,58 +113,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_service/mod.rs similarity index 56% rename from bin/validator/src/server/validate_block.rs rename to bin/validator/src/server/validator_service/mod.rs index d65c0443a5..4741eccc37 100644 --- a/bin/validator/src/server/validate_block.rs +++ b/bin/validator/src/server/validator_service/mod.rs @@ -1,20 +1,32 @@ -use miden_node_db::DatabaseError; +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; 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")] @@ -38,12 +50,67 @@ pub enum BlockValidationError { ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey }, #[error("no chain tip exists")] NoChainTip, + #[error("failed to backup block")] + BlockBackupFailed(#[source] std::io::Error), } -// BLOCK VALIDATION -// ================================================================================================ +// 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), + }) + } -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 +124,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 +134,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 + let (proposed_header, proposed_body) = 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 +155,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 +177,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,22 +186,31 @@ 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, }); } 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. #[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_service/sign_block.rs similarity index 96% rename from bin/validator/src/server/sign_block.rs rename to bin/validator/src/server/validator_service/sign_block.rs index 3ed7ca3b42..e75c6fcfdb 100644 --- a/bin/validator/src/server/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::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); diff --git a/bin/validator/src/server/status.rs b/bin/validator/src/server/validator_service/status.rs similarity index 90% rename from bin/validator/src/server/status.rs rename to bin/validator/src/server/validator_service/status.rs index 078b809113..ddd03b4727 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 93% rename from bin/validator/src/server/tests.rs rename to bin/validator/src/server/validator_service/tests.rs index 993400dcd7..6eeb1dce19 100644 --- a/bin/validator/src/server/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}; @@ -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::{ValidatorError, ValidatorService}; 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: 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 [`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; + let (db, block_store, genesis_header) = setup_db_with_genesis(&key).await; Self { - server: ValidatorServer::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, } @@ -83,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(); @@ -98,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 @@ -124,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()); @@ -134,9 +135,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 = ValidatorService::new(rogue_signer, db, block_store, 0, 0, 0).await; assert!( - matches!(result, Err(BlockValidationError::ValidatorKeyMismatch { .. })), + matches!(result, Err(ValidatorError::ValidatorKeyMismatch { .. })), "expected ValidatorKeyMismatch error", ); } @@ -410,7 +411,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 +486,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" ); } 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; From 9fa9f7afc89641575014e430ef177a0128d04281 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 12 Jun 2026 10:59:29 +1200 Subject: [PATCH 4/5] Add data directory enum --- bin/validator/src/commands/bootstrap.rs | 32 +++++------ bin/validator/src/commands/mod.rs | 6 +- bin/validator/src/commands/start.rs | 4 +- bin/validator/src/data_directory.rs | 76 +++++++++++++++++++++++++ bin/validator/src/lib.rs | 2 + bin/validator/src/server/mod.rs | 9 ++- 6 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 bin/validator/src/data_directory.rs diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index 0d40ab19ed..30d3484f0a 100644 --- a/bin/validator/src/commands/bootstrap.rs +++ b/bin/validator/src/commands/bootstrap.rs @@ -6,12 +6,10 @@ 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, @@ -35,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 @@ -51,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) @@ -82,14 +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(data_directory.to_path_buf().join("blocks"), &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 854c521ad0..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::{ValidatorServer, ValidatorSigner}; +use miden_validator::{DataDirectory, ValidatorServer, ValidatorSigner}; // Starts the validator component. pub async fn start( @@ -14,6 +14,8 @@ pub async fn start( data_directory: PathBuf, sqlite_connection_pool_size: NonZeroUsize, ) -> anyhow::Result<()> { + let data_directory = DataDirectory::load_server(data_directory) + .context("failed to load validator data directory")?; ValidatorServer { address, grpc_options, 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 df7aa120e6..aef40d7546 100644 --- a/bin/validator/src/lib.rs +++ b/bin/validator/src/lib.rs @@ -1,8 +1,10 @@ +pub mod data_directory; pub mod db; mod server; mod signers; mod tx_validation; +pub use data_directory::DataDirectory; pub use server::ValidatorServer; pub use signers::{KmsSigner, ValidatorSigner}; diff --git a/bin/validator/src/server/mod.rs b/bin/validator/src/server/mod.rs index 4ab190950d..946f018267 100644 --- a/bin/validator/src/server/mod.rs +++ b/bin/validator/src/server/mod.rs @@ -1,6 +1,5 @@ use std::net::SocketAddr; use std::num::NonZeroUsize; -use std::path::PathBuf; use anyhow::Context; use miden_node_proto::generated::validator::api_server; @@ -20,7 +19,7 @@ use crate::db::{ load_chain_tip, load_with_pool_size, }; -use crate::{COMPONENT, ValidatorSigner}; +use crate::{COMPONENT, DataDirectory, ValidatorSigner}; mod validator_service; @@ -44,7 +43,7 @@ pub struct ValidatorServer { 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, @@ -60,14 +59,14 @@ impl ValidatorServer { // 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.join("blocks").clone()) + 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. From f056b8b3612aeb2bcba0d9db3c02637e61466084 Mon Sep 17 00:00:00 2001 From: sergerad Date: Fri, 12 Jun 2026 11:49:03 +1200 Subject: [PATCH 5/5] Fix doc string --- bin/validator/src/server/validator_service/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/validator/src/server/validator_service/tests.rs b/bin/validator/src/server/validator_service/tests.rs index 6eeb1dce19..ff7079c934 100644 --- a/bin/validator/src/server/validator_service/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -27,8 +27,8 @@ struct TestValidator { } impl TestValidator { - /// Creates a correctly configured [`Validator`]: 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());