use std::fmt::Display;
use std::num::NonZeroUsize;
use std::sync::Arc;
use accept::AcceptHeaderLayer;
use anyhow::Context;
use miden_node_block_producer::{BlockProducerApi, RpcReadiness, RpcSync};
use miden_node_proto::clients::{
NtxBuilderClient,
RpcClient as SourceRpcClient,
SequencerClient,
ValidatorClient,
};
use miden_node_proto::server::{rpc_api, sequencer_api};
use miden_node_proto_build::rpc_api_descriptor;
use miden_node_store::state::{BlockWriter, ProofWriter, State};
use miden_node_tracing::grpc::grpc_trace_fn;
use miden_node_tracing::info;
use miden_node_tracing::panic::{CatchPanicLayer, catch_panic_layer_fn};
use miden_node_utils::clap::GrpcOptions;
use miden_node_utils::cors::cors_for_grpc_web_layer;
use miden_node_utils::grpc;
use miden_node_utils::shutdown::CancellationToken;
use miden_node_utils::tasks::Tasks;
use miden_protocol::block::BlockNumber;
use rand::RngExt;
use tokio::net::TcpListener;
use tokio_stream::wrappers::TcpListenerStream;
use tonic::metadata::AsciiMetadataValue;
use tonic_reflection::server;
use tonic_web::GrpcWebLayer;
use tower_http::classify::{GrpcCode, GrpcErrorsAsFailures, SharedClassifier};
use tower_http::trace::TraceLayer;
use crate::LOG_TARGET;
use crate::server::api::SequencerInternalService;
use crate::server::health::HealthCheckLayer;
mod accept;
pub(crate) mod api;
mod health;
pub struct Rpc {
pub listener: TcpListener,
pub state: Arc<State>,
pub mode: RpcMode,
pub ntx_builder: Option<NtxBuilderClient>,
pub grpc_options: GrpcOptions,
pub network_tx_auth: Option<AsciiMetadataValue>,
}
#[derive(Clone, Debug)]
pub(crate) struct NetworkTxAuth(pub(crate) AsciiMetadataValue);
pub enum RpcMode {
Sequencer {
block_producer: Box<BlockProducerApi>,
validators: ValidatorClients,
},
FullNode {
source_rpc: Box<SourceRpcClient>,
readiness_threshold: u32,
pre_auth: Option<PreAuthSubmission>,
block_writer: BlockWriter,
proof_writer: ProofWriter,
},
}
#[derive(Clone, Debug)]
pub(crate) enum RpcBackend {
Sequencer {
block_producer: Box<BlockProducerApi>,
validators: ValidatorClients,
},
FullNode {
source_rpc: Box<SourceRpcClient>,
pre_auth: Option<PreAuthSubmission>,
},
}
#[cfg(test)]
impl RpcBackend {
pub(crate) fn sequencer(
block_producer: BlockProducerApi,
validators: ValidatorClients,
) -> Self {
Self::Sequencer {
block_producer: Box::new(block_producer),
validators,
}
}
pub(crate) fn full_node(
source_rpc: SourceRpcClient,
pre_auth: Option<PreAuthSubmission>,
) -> Self {
Self::FullNode {
source_rpc: Box::new(source_rpc),
pre_auth,
}
}
}
#[derive(Clone, Debug)]
pub struct ValidatorClients(Vec<ValidatorClient>);
impl ValidatorClients {
pub fn new(validators: Vec<ValidatorClient>) -> anyhow::Result<Self> {
anyhow::ensure!(!validators.is_empty(), "at least one validator is required");
Ok(Self(validators))
}
pub(crate) fn random(&self) -> &ValidatorClient {
let index = rand::rng().random_range(0..self.0.len());
&self.0[index]
}
pub(crate) fn as_slice(&self) -> &[ValidatorClient] {
&self.0
}
}
#[derive(Clone, Debug)]
pub struct PreAuthSubmission {
validators: ValidatorClients,
sequencer: Box<SequencerClient>,
}
impl PreAuthSubmission {
pub fn new(
validators: Vec<ValidatorClient>,
sequencer: SequencerClient,
) -> anyhow::Result<Self> {
let validators = ValidatorClients::new(validators)
.context("pre-authenticated submission requires at least one validator")?;
Ok(Self {
validators,
sequencer: Box::new(sequencer),
})
}
pub(crate) fn validators(&self) -> &ValidatorClients {
&self.validators
}
pub(crate) fn sequencer(&self) -> &SequencerClient {
&self.sequencer
}
}
impl RpcMode {
pub fn sequencer(block_producer: BlockProducerApi, validators: ValidatorClients) -> Self {
Self::Sequencer {
block_producer: Box::new(block_producer),
validators,
}
}
pub fn full_node(
source_rpc: SourceRpcClient,
readiness_threshold: u32,
pre_auth: Option<PreAuthSubmission>,
block_writer: BlockWriter,
proof_writer: ProofWriter,
) -> Self {
Self::FullNode {
source_rpc: Box::new(source_rpc),
readiness_threshold,
pre_auth,
block_writer,
proof_writer,
}
}
const fn as_str(&self) -> &'static str {
match self {
Self::Sequencer { .. } => "sequencer",
Self::FullNode { .. } => "full",
}
}
fn backend(&self) -> RpcBackend {
match self {
Self::Sequencer { block_producer, validators } => RpcBackend::Sequencer {
block_producer: block_producer.clone(),
validators: validators.clone(),
},
Self::FullNode { source_rpc, pre_auth, .. } => RpcBackend::FullNode {
source_rpc: source_rpc.clone(),
pre_auth: pre_auth.clone(),
},
}
}
}
impl Rpc {
pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
let endpoint = self.listener.local_addr().context("failed to read RPC listen address")?;
let mode = self.mode.as_str();
let mut api = api::RpcService::new(
self.state.clone(),
self.mode.backend(),
self.ntx_builder.clone(),
NonZeroUsize::new(1_000_000).unwrap(),
self.network_tx_auth.map(NetworkTxAuth),
);
let genesis = api
.get_genesis_header_with_retry()
.await
.context("Fetching genesis header from store")?;
api.set_genesis_commitment(genesis.commitment())?;
let api_service = rpc_api::service(api);
let mut tasks = Tasks::new();
let (health_reporter, health_service) = tonic_health::server::health_reporter();
match self.mode {
RpcMode::Sequencer { .. } => {
health_reporter
.set_service_status(
rpc_api::service_name(),
tonic_health::ServingStatus::Serving,
)
.await;
let chain_tip = self.state.committed_tip();
log_node_ready(mode, endpoint, chain_tip);
},
RpcMode::FullNode {
source_rpc,
readiness_threshold,
block_writer,
proof_writer,
..
} => {
Self::spawn_full_node_sync(
&self.state,
&mut tasks,
health_reporter,
mode,
endpoint,
shutdown.clone(),
*source_rpc,
readiness_threshold,
block_writer,
proof_writer,
)
.await;
},
}
let reflection_service = server::Builder::configure()
.register_file_descriptor_set(rpc_api_descriptor())
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
.build_v1()
.context("failed to build reflection service")?;
let rpc_version = env!("CARGO_PKG_VERSION");
let rpc_version =
semver::Version::parse(rpc_version).context("failed to parse crate version")?;
let rpc = tonic::transport::Server::builder()
.accept_http1(true)
.timeout(self.grpc_options.request_timeout)
.layer(CatchPanicLayer::custom(catch_panic_layer_fn))
.layer(
TraceLayer::new(SharedClassifier::new(
GrpcErrorsAsFailures::new()
.with_success(GrpcCode::InvalidArgument)
.with_success(GrpcCode::NotFound)
.with_success(GrpcCode::ResourceExhausted)
.with_success(GrpcCode::Unimplemented)
.with_success(GrpcCode::Unknown),
))
.make_span_with(grpc_trace_fn),
)
.layer(HealthCheckLayer)
.layer(cors_for_grpc_web_layer())
.layer(GrpcWebLayer::new())
.layer(grpc::ResolveClientIpLayer)
.layer(
AcceptHeaderLayer::new(&rpc_version, genesis.commitment())
.with_genesis_enforced_method("SubmitProvenTx")
.with_genesis_enforced_method("SubmitProvenTxBatch"),
)
.add_service(api_service)
.add_service(health_service)
.add_service(reflection_service)
.serve_with_incoming_shutdown(
TcpListenerStream::new(self.listener),
shutdown.clone().cancelled_owned(),
);
tasks.spawn("RPC server", async move { rpc.await.map_err(|e| anyhow::anyhow!(e)) });
tasks.join_next_or_cancelled(shutdown).await
}
#[expect(
clippy::too_many_arguments,
reason = "assembles the full-node sync task from Rpc::serve's local state"
)]
async fn spawn_full_node_sync(
state: &Arc<State>,
tasks: &mut Tasks,
health_reporter: tonic_health::server::HealthReporter,
mode: &str,
endpoint: impl Display,
shutdown: CancellationToken,
source_rpc: SourceRpcClient,
readiness_threshold: u32,
block_writer: BlockWriter,
proof_writer: ProofWriter,
) {
health_reporter
.set_service_status(rpc_api::service_name(), tonic_health::ServingStatus::NotServing)
.await;
let readiness = RpcReadiness::new(health_reporter, readiness_threshold);
tasks.spawn(
"RPC sync",
RpcSync {
state: Arc::clone(state),
block_writer,
proof_writer,
source_rpc,
readiness,
}
.run(shutdown),
);
log_node_synchronizing(mode, endpoint, readiness_threshold);
}
}
fn log_node_ready(mode: &str, endpoint: impl Display, chain_tip: BlockNumber) {
info!(
target: LOG_TARGET,
"Node ready",
service.name = "miden-node",
service.version = env!("CARGO_PKG_VERSION"),
node.role = mode,
rpc.listen = endpoint.to_string(),
block.number = chain_tip
);
}
fn log_node_synchronizing(mode: &str, endpoint: impl Display, readiness_threshold: u32) {
info!(
target: LOG_TARGET,
"Node started; synchronizing",
service.name = "miden-node",
service.version = env!("CARGO_PKG_VERSION"),
node.role = mode,
rpc.listen = endpoint.to_string(),
sync.ready_threshold = readiness_threshold
);
}
pub struct SequencerInternal {
pub listener: TcpListener,
pub state: Arc<State>,
pub block_producer: BlockProducerApi,
pub grpc_options: GrpcOptions,
}
impl SequencerInternal {
pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
let endpoint = self
.listener
.local_addr()
.context("failed to read internal sequencer listen address")?;
info!(
target: LOG_TARGET,
"Internal sequencer server ready",
internal.listen = endpoint.to_string()
);
let service = SequencerInternalService {
state: self.state,
block_producer: self.block_producer,
};
tonic::transport::Server::builder()
.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(sequencer_api::service(service))
.serve_with_incoming_shutdown(
TcpListenerStream::new(self.listener),
shutdown.cancelled_owned(),
)
.await
.context("failed to serve internal sequencer API")
}
}