diff --git a/CHANGELOG.md b/CHANGELOG.md index cf89204f6..7489b5550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [BREAKING] Renamed `GetNoteError` endpoint to `GetNetworkNoteStatus` and extended it to return the full lifecycle status of a network note (`Pending`, `Processed`, `Discarded`, `Committed`) instead of only error information. Consumed notes are now retained in the database after block commit instead of being deleted ([#1892](https://github.com/0xMiden/node/pull/1892)). - Extended `ValidatorStatus` proto response with `chain_tip`, `validated_transactions_count`, and `signed_blocks_count`; added Validator card to the network monitor dashboard ([#1900](https://github.com/0xMiden/node/pull/1900)). - Updated the RocksDB SMT backend to use budgeted deserialization for bytes read from disk, ported from `0xMiden/crypto` PR [#846](https://github.com/0xMiden/crypto/pull/846) ([#1923](https://github.com/0xMiden/node/pull/1923)). +- Refactored the validator gRPC API implementation to use the new per-method trait implementations ([#1959](https://github.com/0xMiden/node/pull/1959)). ## v0.14.9 (2026-04-21) diff --git a/crates/validator/src/server/mod.rs b/crates/validator/src/server/mod.rs index 2b0722a5c..be2920b45 100644 --- a/crates/validator/src/server/mod.rs +++ b/crates/validator/src/server/mod.rs @@ -1,44 +1,31 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +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::generated::{self as proto}; use miden_node_proto_build::validator_api_descriptor; -use miden_node_utils::ErrorReport; use miden_node_utils::clap::GrpcOptionsInternal; use miden_node_utils::panic::catch_panic_layer_fn; -use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_node_utils::tracing::grpc::grpc_trace_fn; -use miden_protocol::block::ProposedBlock; -use miden_protocol::transaction::{ProvenTransaction, TransactionInputs}; -use miden_protocol::utils::serde::{Deserializable, Serializable}; use tokio::net::TcpListener; use tokio::sync::Semaphore; use tokio_stream::wrappers::TcpListenerStream; -use tonic::Status; use tower_http::catch_panic::CatchPanicLayer; use tower_http::trace::TraceLayer; -use tracing::{info_span, instrument}; -use crate::block_validation::validate_block; -use crate::db::{ - count_signed_blocks, - count_validated_transactions, - insert_transaction, - load, - load_chain_tip, - upsert_block_header, -}; -use crate::tx_validation::validate_transaction; +use crate::db::{count_signed_blocks, count_validated_transactions, load, load_chain_tip}; use crate::{COMPONENT, ValidatorSigner}; #[cfg(test)] mod tests; +mod sign_block; +mod status; +mod submit_proven_transaction; + // VALIDATOR // ================================================================================ @@ -150,125 +137,3 @@ impl ValidatorServer { } } } - -#[tonic::async_trait] -impl api_server::Api for ValidatorServer { - /// Returns the status of the validator. - async fn status( - &self, - _request: tonic::Request<()>, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new(proto::validator::ValidatorStatus { - version: env!("CARGO_PKG_VERSION").to_string(), - status: "OK".to_string(), - chain_tip: self.chain_tip.load(Ordering::Relaxed), - validated_transactions_count: self.validated_transactions_count.load(Ordering::Relaxed), - signed_blocks_count: self.signed_blocks_count.load(Ordering::Relaxed), - })) - } - - /// Receives a proven transaction, then validates and stores it. - #[instrument(target = COMPONENT, skip_all, err)] - async fn submit_proven_transaction( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let (tx, inputs) = info_span!("deserialize").in_scope(|| { - let request = request.into_inner(); - let tx = ProvenTransaction::read_from_bytes(&request.transaction).map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid proven transaction")) - })?; - let inputs = request - .transaction_inputs - .ok_or(Status::invalid_argument("Missing transaction inputs"))?; - let inputs = TransactionInputs::read_from_bytes(&inputs).map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid transaction inputs")) - })?; - - Result::<_, tonic::Status>::Ok((tx, inputs)) - })?; - - tracing::Span::current().set_attribute("transaction.id", tx.id()); - - // Validate the transaction. - let tx_info = validate_transaction(tx, inputs).await.map_err(|err| { - Status::invalid_argument(err.as_report_context("Invalid transaction")) - })?; - - // Store the validated transaction. - let count = self - .db - .transact("insert_transaction", move |conn| insert_transaction(conn, &tx_info)) - .await - .map_err(|err| { - Status::internal(err.as_report_context("Failed to insert transaction")) - })?; - - self.validated_transactions_count.fetch_add(count as u64, Ordering::Relaxed); - - Ok(tonic::Response::new(())) - } - - /// Validates a proposed block, verifies chain continuity, signs the block header, and updates - /// the chain tip. - async fn sign_block( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let proposed_block = info_span!("deserialize").in_scope(|| { - let proposed_block_bytes = request.into_inner().proposed_block; - - ProposedBlock::read_from_bytes(&proposed_block_bytes).map_err(|err| { - tonic::Status::invalid_argument(format!( - "Failed to deserialize proposed block: {err}", - )) - }) - })?; - - // Serialize sign_block requests to prevent race conditions between loading the - // chain tip and persisting the validated block header. - let _permit = self.sign_block_semaphore.acquire().await.map_err(|err| { - tonic::Status::internal(format!("sign_block semaphore closed: {err}")) - })?; - - // Load the current chain tip from the database. - let chain_tip = self - .db - .query("load_chain_tip", load_chain_tip) - .await - .map_err(|err| { - tonic::Status::internal(format!("Failed to load chain tip: {}", err.as_report())) - })? - .ok_or_else(|| tonic::Status::internal("Chain tip not found in database"))?; - - // Validate the block against the current chain tip. - let (signature, header) = validate_block(proposed_block, &self.signer, &self.db, chain_tip) - .await - .map_err(|err| { - tonic::Status::invalid_argument(format!( - "Failed to validate block: {}", - err.as_report() - )) - })?; - - // 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)) - .await - .map_err(|err| { - tonic::Status::internal(format!( - "Failed to persist block header: {}", - err.as_report() - )) - })?; - - // Update the in-memory counters after successful persistence. - self.chain_tip.store(new_block_num, Ordering::Relaxed); - self.signed_blocks_count.fetch_add(1, Ordering::Relaxed); - - // Send the signature. - let response = proto::blockchain::BlockSignature { signature: signature.to_bytes() }; - Ok(tonic::Response::new(response)) - } -} diff --git a/crates/validator/src/server/sign_block.rs b/crates/validator/src/server/sign_block.rs new file mode 100644 index 000000000..a9f288b60 --- /dev/null +++ b/crates/validator/src/server/sign_block.rs @@ -0,0 +1,75 @@ +use std::sync::atomic::Ordering; + +use miden_node_proto::generated as grpc; +use miden_node_utils::ErrorReport; +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; + +#[tonic::async_trait] +impl grpc::server::validator_api::SignBlock for ValidatorServer { + type Input = ProposedBlock; + type Output = Signature; + + fn decode(request: grpc::blockchain::ProposedBlock) -> tonic::Result { + ProposedBlock::read_from_bytes(&request.proposed_block).map_err(|err| { + tonic::Status::invalid_argument( + err.as_report_context("Failed to deserialize proposed block"), + ) + }) + } + + fn encode(output: Self::Output) -> tonic::Result { + Ok(grpc::blockchain::BlockSignature { signature: output.to_bytes() }) + } + + async fn handle(&self, proposed_block: Self::Input) -> tonic::Result { + // Serialize sign_block requests to prevent race conditions between loading the + // chain tip and persisting the validated block header. + let _permit = self.sign_block_semaphore.acquire().await.map_err(|err| { + tonic::Status::internal(format!("sign_block semaphore closed: {err}")) + })?; + + // Load the current chain tip from the database. + let chain_tip = self + .db + .query("load_chain_tip", load_chain_tip) + .await + .map_err(|err| { + tonic::Status::internal(format!("Failed to load chain tip: {}", err.as_report())) + })? + .ok_or_else(|| tonic::Status::internal("Chain tip not found in database"))?; + + // Validate the block against the current chain tip. + let (signature, header) = validate_block(proposed_block, &self.signer, &self.db, chain_tip) + .await + .map_err(|err| { + tonic::Status::invalid_argument(format!( + "Failed to validate block: {}", + err.as_report() + )) + })?; + + // 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)) + .await + .map_err(|err| { + tonic::Status::internal(format!( + "Failed to persist block header: {}", + err.as_report() + )) + })?; + + // Update the in-memory counters after successful persistence. + self.chain_tip.store(new_block_num, Ordering::Relaxed); + self.signed_blocks_count.fetch_add(1, Ordering::Relaxed); + + Ok(signature) + } +} diff --git a/crates/validator/src/server/status.rs b/crates/validator/src/server/status.rs new file mode 100644 index 000000000..078b80911 --- /dev/null +++ b/crates/validator/src/server/status.rs @@ -0,0 +1,33 @@ +use std::sync::atomic::Ordering; + +use miden_node_proto::generated as grpc; + +use crate::server::ValidatorServer; + +#[tonic::async_trait] +impl grpc::server::validator_api::Status for ValidatorServer { + type Input = (); + type Output = (); + + async fn full(&self, _request: ()) -> tonic::Result { + Ok(grpc::validator::ValidatorStatus { + version: env!("CARGO_PKG_VERSION").to_string(), + status: "OK".to_string(), + chain_tip: self.chain_tip.load(Ordering::Relaxed), + validated_transactions_count: self.validated_transactions_count.load(Ordering::Relaxed), + signed_blocks_count: self.signed_blocks_count.load(Ordering::Relaxed), + }) + } + + async fn handle(&self, _input: Self::Input) -> tonic::Result { + unimplemented!() + } + + fn decode(_request: ()) -> tonic::Result { + unimplemented!() + } + + fn encode(_output: Self::Output) -> tonic::Result { + unimplemented!() + } +} diff --git a/crates/validator/src/server/submit_proven_transaction.rs b/crates/validator/src/server/submit_proven_transaction.rs new file mode 100644 index 000000000..d5d7d9b21 --- /dev/null +++ b/crates/validator/src/server/submit_proven_transaction.rs @@ -0,0 +1,62 @@ +use std::sync::atomic::Ordering; + +use miden_node_proto::generated as grpc; +use miden_node_utils::ErrorReport; +use miden_node_utils::tracing::OpenTelemetrySpanExt; +use miden_protocol::transaction::{ProvenTransaction, TransactionInputs}; +use miden_tx::utils::serde::Deserializable; +use tonic::Status; + +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 { + type Input = Input; + type Output = (); + + async fn handle(&self, input: Self::Input) -> tonic::Result { + tracing::Span::current().set_attribute("transaction.id", input.tx.id()); + + // Validate the transaction. + let tx_info = validate_transaction(input.tx, input.inputs).await.map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid transaction")) + })?; + + // Store the validated transaction. + let count = self + .db + .transact("insert_transaction", move |conn| insert_transaction(conn, &tx_info)) + .await + .map_err(|err| { + Status::internal(err.as_report_context("Failed to insert transaction")) + })?; + + self.validated_transactions_count.fetch_add(count as u64, Ordering::Relaxed); + Ok(()) + } + + fn decode(request: grpc::transaction::ProvenTransaction) -> tonic::Result { + let tx = ProvenTransaction::read_from_bytes(&request.transaction).map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid proven transaction")) + })?; + let inputs = request + .transaction_inputs + .ok_or(Status::invalid_argument("Missing transaction inputs"))?; + let inputs = TransactionInputs::read_from_bytes(&inputs).map_err(|err| { + Status::invalid_argument(err.as_report_context("Invalid transaction inputs")) + })?; + + Ok(Self::Input { tx, inputs }) + } + + fn encode(output: Self::Output) -> tonic::Result<()> { + Ok(output) + } +} + +pub struct Input { + tx: ProvenTransaction, + inputs: TransactionInputs, +}