From 7c739f38cae4351fc2a8333d02011d6b62e10fd0 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 17 Dec 2025 14:02:06 +1300 Subject: [PATCH 1/9] Add block validation logic --- crates/validator/src/block_validation/mod.rs | 60 ++++++++++++++++++++ crates/validator/src/lib.rs | 1 + crates/validator/src/server/mod.rs | 43 ++++++++++---- crates/validator/src/tx_validation/mod.rs | 6 +- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 crates/validator/src/block_validation/mod.rs diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs new file mode 100644 index 000000000..93c5e1bd3 --- /dev/null +++ b/crates/validator/src/block_validation/mod.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use miden_lib::block::build_block; +use miden_objects::ProposedBlockError; +use miden_objects::block::{BlockNumber, BlockSigner, ProposedBlock}; +use miden_objects::crypto::dsa::ecdsa_k256_keccak::Signature; +use miden_objects::transaction::TransactionId; + +use crate::server::ValidatedTransactions; + +// BLOCK VALIDATION ERROR +// ================================================================================================ + +#[derive(thiserror::Error, Debug)] +pub enum BlockValidationError { + #[error("transaction {0} in block {1} has not been validated")] + TransactionNotValidated(TransactionId, BlockNumber), + #[error("failed to build block")] + BlockBuildingFailed(#[from] ProposedBlockError), +} + +// BLOCK VALIDATION +// ================================================================================================ + +/// Validates a block by checking that all transactions in the proposed block have been processed by +/// the validator in the past. +/// +/// Removes the validated transactions from the cache upon success. +pub async fn validate_block( + proposed_block: ProposedBlock, + signer: &S, + validated_transactions: Arc, +) -> Result { + // Build the block. + let (header, body) = build_block(proposed_block)?; + + // Check that all transactions in the proposed block have been validated + let validated_txs = validated_transactions.read().await; + for tx_header in body.transactions().as_slice() { + let tx_id = tx_header.id(); + if !validated_txs.contains_key(&tx_id) { + return Err(BlockValidationError::TransactionNotValidated(tx_id, header.block_num())); + } + } + // Release the validated transactions read lock. + drop(validated_txs); + + // Sign the header. + let signature = signer.sign(&header); + + // Remove the validated transactions from the cache. + let mut validated_txs = validated_transactions.write().await; + for tx_header in body.transactions().as_slice() { + validated_txs.remove(&tx_header.id()); + } + // Release the validated transactions write lock. + drop(validated_txs); + + Ok(signature) +} diff --git a/crates/validator/src/lib.rs b/crates/validator/src/lib.rs index 065098d8a..a45112d27 100644 --- a/crates/validator/src/lib.rs +++ b/crates/validator/src/lib.rs @@ -1,3 +1,4 @@ +mod block_validation; mod server; mod tx_validation; diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 5e950be67..a4fcbec6c 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,8 +1,9 @@ +use std::collections::HashMap; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use anyhow::Context; -use miden_lib::block::build_block; use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; @@ -10,17 +11,27 @@ use miden_node_utils::ErrorReport; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::grpc::grpc_trace_fn; use miden_objects::block::{BlockSigner, ProposedBlock}; -use miden_objects::transaction::{ProvenTransaction, TransactionInputs}; +use miden_objects::transaction::{ + ProvenTransaction, + TransactionHeader, + TransactionId, + TransactionInputs, +}; use miden_objects::utils::{Deserializable, Serializable}; use tokio::net::TcpListener; +use tokio::sync::RwLock; use tokio_stream::wrappers::TcpListenerStream; use tonic::Status; use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; use crate::COMPONENT; +use crate::block_validation::validate_block; use crate::tx_validation::validate_transaction; +/// A type alias for a read-write lock that stores validated transactions. +pub type ValidatedTransactions = RwLock>; + // VALIDATOR // ================================================================================ @@ -70,7 +81,10 @@ impl Validator { .layer(CatchPanicLayer::custom(catch_panic_layer_fn)) .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn)) .timeout(self.grpc_timeout) - .add_service(api_server::ApiServer::new(ValidatorServer { signer: self.signer })) + .add_service(api_server::ApiServer::new(ValidatorServer { + signer: self.signer, + validated_transactions: ValidatedTransactions::default().into(), + })) .add_service(reflection_service) .add_service(reflection_service_alpha) .serve_with_incoming(TcpListenerStream::new(listener)) @@ -87,6 +101,7 @@ impl Validator { /// Implements the gRPC API for the validator. struct ValidatorServer { signer: S, + validated_transactions: Arc, } #[tonic::async_trait] @@ -123,9 +138,14 @@ impl api_server::Api for ValidatorServer })?; // Validate the transaction. - validate_transaction(proven_tx, tx_inputs).await.map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid transaction")) - })?; + let validated_tx_header = + validate_transaction(proven_tx.clone(), tx_inputs).await.map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid transaction")) + })?; + + // Register the validated transaction. + let tx_id = validated_tx_header.id(); + self.validated_transactions.write().await.insert(tx_id, validated_tx_header); Ok(tonic::Response::new(())) } @@ -145,10 +165,13 @@ impl api_server::Api for ValidatorServer )) })?; - // Build and sign header. - let (header, _body) = build_block(proposed_block) - .map_err(|err| tonic::Status::internal(format!("Failed to build block: {err}")))?; - let signature = self.signer.sign(&header); + // Validate the block. + let signature = + validate_block(proposed_block, &self.signer, self.validated_transactions.clone()) + .await + .map_err(|err| { + tonic::Status::invalid_argument(format!("Failed to validate block: {err}",)) + })?; // Send the signature. let response = proto::blockchain::BlockSignature { signature: signature.to_bytes() }; diff --git a/crates/validator/src/tx_validation/mod.rs b/crates/validator/src/tx_validation/mod.rs index 188fe2f6b..1661310c2 100644 --- a/crates/validator/src/tx_validation/mod.rs +++ b/crates/validator/src/tx_validation/mod.rs @@ -27,10 +27,12 @@ pub enum TransactionValidationError { /// Validates a transaction by verifying its proof, executing it and comparing its header with the /// provided proven transaction. +/// +/// Returns the header of the executed transaction if successful. pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, -) -> Result<(), TransactionValidationError> { +) -> Result { // First, verify the transaction proof let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); tx_verifier.verify(&proven_tx)?; @@ -50,7 +52,7 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - Ok(()) + Ok(executed_tx_header) } else { Err(TransactionValidationError::Mismatch { proven_tx_header: proven_tx_header.into(), From 461a3dba0d356a49fd696722efd3daa5ad12b0ce Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 17 Dec 2025 15:46:22 +1300 Subject: [PATCH 2/9] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be32f2bf..278740b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - The mempool's transaction capacity is now configurable ([#1433](https://github.com/0xMiden/miden-node/pull/1433)). - Renamed card's names in the `miden-network-monitor` binary ([#1441](https://github.com/0xMiden/miden-node/pull/1441)). - Integrated RPC stack with Validator component for transaction validation ([#1457](https://github.com/0xMiden/miden-node/pull/1457)). +- Added validated transactions check to block validation logc in Validator ([#1459](https://github.com/0xMiden/miden-node/pull/1459)). ### Changes From e65d0e446d1cf31f3665121433131af168112ec7 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 17 Dec 2025 15:52:38 +1300 Subject: [PATCH 3/9] ValidatorServer::new --- crates/validator/src/server/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index a4fcbec6c..2fbabcbea 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -81,10 +81,7 @@ impl Validator { .layer(CatchPanicLayer::custom(catch_panic_layer_fn)) .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn)) .timeout(self.grpc_timeout) - .add_service(api_server::ApiServer::new(ValidatorServer { - signer: self.signer, - validated_transactions: ValidatedTransactions::default().into(), - })) + .add_service(api_server::ApiServer::new(ValidatorServer::new(self.signer))) .add_service(reflection_service) .add_service(reflection_service_alpha) .serve_with_incoming(TcpListenerStream::new(listener)) @@ -104,6 +101,13 @@ struct ValidatorServer { validated_transactions: Arc, } +impl ValidatorServer { + fn new(signer: S) -> Self { + let validated_transactions = Arc::new(ValidatedTransactions::default()); + Self { signer, validated_transactions } + } +} + #[tonic::async_trait] impl api_server::Api for ValidatorServer { /// Returns the status of the validator. From 101ffc3c531d3c95fd26116a037a3838e1df22e5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Wed, 17 Dec 2025 15:53:13 +1300 Subject: [PATCH 4/9] Update changelog PR # --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278740b8b..7c23cdade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - The mempool's transaction capacity is now configurable ([#1433](https://github.com/0xMiden/miden-node/pull/1433)). - Renamed card's names in the `miden-network-monitor` binary ([#1441](https://github.com/0xMiden/miden-node/pull/1441)). - Integrated RPC stack with Validator component for transaction validation ([#1457](https://github.com/0xMiden/miden-node/pull/1457)). -- Added validated transactions check to block validation logc in Validator ([#1459](https://github.com/0xMiden/miden-node/pull/1459)). +- Added validated transactions check to block validation logc in Validator ([#1460](https://github.com/0xMiden/miden-node/pull/1460)). ### Changes From 4016a7907327817fedbfef68e4919adeadb7583c Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 5 Jan 2026 09:50:24 +1300 Subject: [PATCH 5/9] RM unnecessary clone --- crates/validator/src/server/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index ee8194a31..b32ac0ffe 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -143,7 +143,7 @@ impl api_server::Api for ValidatorServer // Validate the transaction. let validated_tx_header = - validate_transaction(proven_tx.clone(), tx_inputs).await.map_err(|err| { + validate_transaction(proven_tx, tx_inputs).await.map_err(|err| { Status::invalid_argument(err.as_report_context("Invalid transaction")) })?; From 8a12ebcef11e3bb6734b5e354121c73acca8a7f5 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 5 Jan 2026 09:54:58 +1300 Subject: [PATCH 6/9] Return error on re-execution failure --- crates/rpc/src/server/api.rs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 7dc17e196..86bb35e59 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -25,7 +25,7 @@ use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{MIN_PROOF_SECURITY_LEVEL, Word}; use miden_tx::TransactionVerifier; use tonic::{IntoRequest, Request, Response, Status}; -use tracing::{debug, info, instrument, warn}; +use tracing::{debug, info, instrument}; use url::Url; use crate::COMPONENT; @@ -392,23 +392,7 @@ impl api_server::Api for RpcService { // If transaction inputs are provided, re-execute the transaction to validate it. if request.transaction_inputs.is_some() { // Re-execute the transaction via the Validator. - match self.validator.clone().submit_proven_transaction(request.clone()).await { - Ok(_) => { - debug!( - target = COMPONENT, - tx_id = %tx.id().to_hex(), - "Transaction validation successful" - ); - }, - Err(e) => { - warn!( - target = COMPONENT, - tx_id = %tx.id().to_hex(), - error = %e, - "Transaction validation failed, but continuing with submission" - ); - }, - } + self.validator.clone().submit_proven_transaction(request.clone()).await?; } block_producer.clone().submit_proven_transaction(request).await From 62fe142b85a9203fd3e84eabfc6fe094a3c87435 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 5 Jan 2026 10:01:56 +1300 Subject: [PATCH 7/9] Build block after tx validation --- crates/validator/src/block_validation/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index f2602f064..766cc582a 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -30,20 +30,23 @@ pub async fn validate_block( signer: &S, validated_transactions: Arc, ) -> Result { - // Build the block. - let (header, body) = proposed_block.into_header_and_body()?; - // Check that all transactions in the proposed block have been validated let validated_txs = validated_transactions.read().await; - for tx_header in body.transactions().as_slice() { + for tx_header in proposed_block.transactions() { let tx_id = tx_header.id(); if !validated_txs.contains_key(&tx_id) { - return Err(BlockValidationError::TransactionNotValidated(tx_id, header.block_num())); + return Err(BlockValidationError::TransactionNotValidated( + tx_id, + proposed_block.block_num(), + )); } } // Release the validated transactions read lock. drop(validated_txs); + // Build the block. + let (header, body) = proposed_block.into_header_and_body()?; + // Sign the header. let signature = signer.sign(&header); From dce38b6a3127ddb67d75989e03e8462a3b60ea2c Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 5 Jan 2026 10:15:03 +1300 Subject: [PATCH 8/9] Use LruCache --- crates/ntx-builder/src/actor/execute.rs | 20 +++++++++----------- crates/utils/src/lru_cache.rs | 2 +- crates/validator/src/block_validation/mod.rs | 17 +++-------------- crates/validator/src/server/mod.rs | 16 ++++++++++------ 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/crates/ntx-builder/src/actor/execute.rs b/crates/ntx-builder/src/actor/execute.rs index 3c5eeb702..83c1d09c9 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -412,25 +412,23 @@ impl DataStore for NtxDataStore { &self, script_root: Word, ) -> impl FutureMaybeSend, DataStoreError>> { - let store = self.store.clone(); - let mut cache = self.script_cache.clone(); - async move { // Attempt to retrieve the script from the cache. - if let Some(cached_script) = cache.get(&script_root).await { + if let Some(cached_script) = self.script_cache.get(&script_root).await { return Ok(Some(cached_script)); } // Retrieve the script from the store. - let maybe_script = store.get_note_script_by_root(script_root).await.map_err(|err| { - DataStoreError::Other { - error_msg: "failed to retrieve note script from store".to_string().into(), - source: Some(err.into()), - } - })?; + let maybe_script = + self.store.get_note_script_by_root(script_root).await.map_err(|err| { + DataStoreError::Other { + error_msg: "failed to retrieve note script from store".to_string().into(), + source: Some(err.into()), + } + })?; // Handle response. if let Some(script) = maybe_script { - cache.put(script_root, script.clone()).await; + self.script_cache.put(script_root, script.clone()).await; Ok(Some(script)) } else { Ok(None) diff --git a/crates/utils/src/lru_cache.rs b/crates/utils/src/lru_cache.rs index de325bf10..7e6751529 100644 --- a/crates/utils/src/lru_cache.rs +++ b/crates/utils/src/lru_cache.rs @@ -26,7 +26,7 @@ where } /// Puts a value into the cache. - pub async fn put(&mut self, key: K, value: V) { + pub async fn put(&self, key: K, value: V) { self.0.lock().await.put(key, value); } } diff --git a/crates/validator/src/block_validation/mod.rs b/crates/validator/src/block_validation/mod.rs index 766cc582a..68744010f 100644 --- a/crates/validator/src/block_validation/mod.rs +++ b/crates/validator/src/block_validation/mod.rs @@ -31,32 +31,21 @@ pub async fn validate_block( validated_transactions: Arc, ) -> Result { // Check that all transactions in the proposed block have been validated - let validated_txs = validated_transactions.read().await; for tx_header in proposed_block.transactions() { let tx_id = tx_header.id(); - if !validated_txs.contains_key(&tx_id) { + if validated_transactions.get(&tx_id).await.is_none() { return Err(BlockValidationError::TransactionNotValidated( tx_id, proposed_block.block_num(), )); } } - // Release the validated transactions read lock. - drop(validated_txs); - // Build the block. - let (header, body) = proposed_block.into_header_and_body()?; + // Build the block header. + let (header, _) = proposed_block.into_header_and_body()?; // Sign the header. let signature = signer.sign(&header); - // Remove the validated transactions from the cache. - let mut validated_txs = validated_transactions.write().await; - for tx_header in body.transactions().as_slice() { - validated_txs.remove(&tx_header.id()); - } - // Release the validated transactions write lock. - drop(validated_txs); - Ok(signature) } diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index b32ac0ffe..0c48de745 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use std::net::SocketAddr; +use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; @@ -8,6 +8,7 @@ use miden_node_proto::generated::validator::api_server; use miden_node_proto::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; use miden_node_utils::ErrorReport; +use miden_node_utils::lru_cache::LruCache; use miden_node_utils::panic::catch_panic_layer_fn; use miden_node_utils::tracing::grpc::grpc_trace_fn; use miden_protocol::block::{BlockSigner, ProposedBlock}; @@ -19,7 +20,6 @@ use miden_protocol::transaction::{ }; use miden_tx::utils::{Deserializable, Serializable}; use tokio::net::TcpListener; -use tokio::sync::RwLock; use tokio_stream::wrappers::TcpListenerStream; use tonic::Status; use tower_http::catch_panic::CatchPanicLayer; @@ -29,8 +29,11 @@ use crate::COMPONENT; use crate::block_validation::validate_block; use crate::tx_validation::validate_transaction; -/// A type alias for a read-write lock that stores validated transactions. -pub type ValidatedTransactions = RwLock>; +/// Number of transactions to keep in the validated transactions cache. +const NUM_VALIDATED_TRANSACTIONS: NonZeroUsize = NonZeroUsize::new(7000).unwrap(); + +/// A type alias for a LRU cache that stores validated transactions. +pub type ValidatedTransactions = LruCache; // VALIDATOR // ================================================================================ @@ -103,7 +106,8 @@ struct ValidatorServer { impl ValidatorServer { fn new(signer: S) -> Self { - let validated_transactions = Arc::new(ValidatedTransactions::default()); + let validated_transactions = + Arc::new(ValidatedTransactions::new(NUM_VALIDATED_TRANSACTIONS)); Self { signer, validated_transactions } } } @@ -149,7 +153,7 @@ impl api_server::Api for ValidatorServer // Register the validated transaction. let tx_id = validated_tx_header.id(); - self.validated_transactions.write().await.insert(tx_id, validated_tx_header); + self.validated_transactions.put(tx_id, validated_tx_header).await; Ok(tonic::Response::new(())) } From 9bb671f992fff3a17a88933ccdf99468fa7423f9 Mon Sep 17 00:00:00 2001 From: sergerad Date: Mon, 5 Jan 2026 10:37:24 +1300 Subject: [PATCH 9/9] Update bundled command --- bin/node/src/commands/bundled.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/node/src/commands/bundled.rs b/bin/node/src/commands/bundled.rs index 6745db617..a51c191eb 100644 --- a/bin/node/src/commands/bundled.rs +++ b/bin/node/src/commands/bundled.rs @@ -102,9 +102,10 @@ pub enum BundledCommand { #[arg( long = "validator.insecure.secret-key", env = ENV_VALIDATOR_INSECURE_SECRET_KEY, - value_name = "VALIDATOR_INSECURE_SECRET_KEY" + value_name = "VALIDATOR_INSECURE_SECRET_KEY", + default_value = INSECURE_VALIDATOR_KEY_HEX )] - validator_insecure_secret_key: Option, + validator_insecure_secret_key: String, }, } @@ -137,9 +138,8 @@ impl BundledCommand { grpc_timeout, validator_insecure_secret_key, } => { - let secret_key_hex = - validator_insecure_secret_key.unwrap_or(INSECURE_VALIDATOR_KEY_HEX.into()); - let signer = SecretKey::read_from_bytes(hex::decode(secret_key_hex)?.as_ref())?; + let secret_key_bytes = hex::decode(validator_insecure_secret_key)?; + let signer = SecretKey::read_from_bytes(&secret_key_bytes)?; Self::start( rpc_url, data_directory,