Skip to main content

miden_node_rpc/server/
mod.rs

1use std::num::NonZeroUsize;
2use std::sync::Arc;
3
4use accept::AcceptHeaderLayer;
5use anyhow::Context;
6use miden_node_block_producer::{BlockProducerApi, RpcReadiness, RpcSync};
7use miden_node_proto::clients::{
8    NtxBuilderClient,
9    RpcClient as SourceRpcClient,
10    SequencerClient,
11    ValidatorClient,
12};
13use miden_node_proto::server::{rpc_api, sequencer_api};
14use miden_node_proto_build::rpc_api_descriptor;
15use miden_node_store::state::State;
16use miden_node_utils::clap::{GrpcOptionsExternal, GrpcOptionsInternal};
17use miden_node_utils::cors::cors_for_grpc_web_layer;
18use miden_node_utils::grpc;
19use miden_node_utils::panic::{CatchPanicLayer, catch_panic_layer_fn};
20use miden_node_utils::shutdown::CancellationToken;
21use miden_node_utils::tasks::Tasks;
22use miden_node_utils::tracing::grpc::grpc_trace_fn;
23use tokio::net::TcpListener;
24use tokio_stream::wrappers::TcpListenerStream;
25use tonic::metadata::AsciiMetadataValue;
26use tonic_reflection::server;
27use tonic_web::GrpcWebLayer;
28use tower_http::classify::{GrpcCode, GrpcErrorsAsFailures, SharedClassifier};
29use tower_http::trace::TraceLayer;
30use tracing::info;
31
32use crate::server::api::SequencerInternalService;
33use crate::server::health::HealthCheckLayer;
34use crate::{COMPONENT, LOG_TARGET};
35
36mod accept;
37pub(crate) mod api;
38mod health;
39
40/// The RPC server component.
41///
42/// On startup, binds to the provided listener and starts serving the RPC API.
43/// It uses the supplied store state and mode-specific submission handling.
44pub struct Rpc {
45    pub listener: TcpListener,
46    pub store: Arc<State>,
47    pub mode: RpcMode,
48    pub ntx_builder: Option<NtxBuilderClient>,
49    pub grpc_options: GrpcOptionsExternal,
50    pub network_tx_auth: Option<AsciiMetadataValue>,
51}
52
53#[derive(Clone, Debug)]
54/// Shared secret value expected in the fixed `x-miden-network-tx-auth` metadata header.
55pub(crate) struct NetworkTxAuth(pub(crate) AsciiMetadataValue);
56
57#[derive(Clone, Debug)]
58pub enum RpcMode {
59    /// Sequencer RPC validates submissions locally, re-executes them through the validator, then
60    /// forwards them to the block producer.
61    Sequencer {
62        block_producer: Box<BlockProducerApi>,
63        validator: Box<ValidatorClient>,
64    },
65    /// Full-node RPC.
66    ///
67    /// By default it forwards submissions verbatim to the source RPC (the caller is responsible for
68    /// configuring this client with any request metadata the source RPC requires).
69    ///
70    /// When the validator and sequencer clients are set, the full-node will, instead of forwarding,
71    /// re-execute submissions through the validator and authenticate them against its store, then
72    /// submit the authenticated result directly to the sequencer's internal API.
73    FullNode {
74        source_rpc: Box<SourceRpcClient>,
75        readiness_threshold: u32,
76        validator: Option<Box<ValidatorClient>>,
77        sequencer: Option<Box<SequencerClient>>,
78    },
79}
80
81impl RpcMode {
82    pub fn sequencer(block_producer: BlockProducerApi, validator: ValidatorClient) -> Self {
83        Self::Sequencer {
84            block_producer: Box::new(block_producer),
85            validator: Box::new(validator),
86        }
87    }
88
89    pub fn full_node(
90        source_rpc: SourceRpcClient,
91        readiness_threshold: u32,
92        validator: Option<ValidatorClient>,
93        sequencer: Option<SequencerClient>,
94    ) -> Self {
95        Self::FullNode {
96            source_rpc: Box::new(source_rpc),
97            readiness_threshold,
98            sequencer: sequencer.map(Box::new),
99            validator: validator.map(Box::new),
100        }
101    }
102}
103
104impl Rpc {
105    /// Serves the RPC API.
106    ///
107    /// In full-node mode, also runs the block/proof sync loop concurrently. Either component
108    /// failing causes both to stop.
109    ///
110    /// Note: Executes in place (i.e. not spawned) and will run indefinitely until
111    ///       a fatal error is encountered.
112    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
113        let mut api = api::RpcService::new(
114            self.store.clone(),
115            self.mode.clone(),
116            self.ntx_builder.clone(),
117            NonZeroUsize::new(1_000_000).unwrap(),
118            self.network_tx_auth.map(NetworkTxAuth),
119        );
120
121        let genesis = api
122            .get_genesis_header_with_retry()
123            .await
124            .context("Fetching genesis header from store")?;
125
126        api.set_genesis_commitment(genesis.commitment())?;
127
128        let api_service = rpc_api::service(api);
129
130        info!(target: LOG_TARGET, endpoint=?self.listener, mode=?self.mode, "Server initialized");
131
132        let mut tasks = Tasks::new();
133
134        // Initialize health reporter and sync service based on the RPC mode.
135        let (health_reporter, health_service) = tonic_health::server::health_reporter();
136        match self.mode {
137            RpcMode::Sequencer { .. } => {
138                health_reporter
139                    .set_service_status(
140                        rpc_api::service_name(),
141                        tonic_health::ServingStatus::Serving,
142                    )
143                    .await;
144            },
145            RpcMode::FullNode { source_rpc, readiness_threshold, .. } => {
146                health_reporter
147                    .set_service_status(
148                        rpc_api::service_name(),
149                        tonic_health::ServingStatus::NotServing,
150                    )
151                    .await;
152                let readiness = RpcReadiness::new(health_reporter, readiness_threshold);
153                tasks.spawn(
154                    "RPC sync",
155                    RpcSync {
156                        state: Arc::clone(&self.store),
157                        source_rpc: *source_rpc,
158                        readiness,
159                    }
160                    .run(shutdown.clone()),
161                );
162            },
163        }
164
165        let reflection_service = server::Builder::configure()
166            .register_file_descriptor_set(rpc_api_descriptor())
167            .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
168            .build_v1()
169            .context("failed to build reflection service")?;
170
171        let rpc_version = env!("CARGO_PKG_VERSION");
172        let rpc_version =
173            semver::Version::parse(rpc_version).context("failed to parse crate version")?;
174
175        let rpc = tonic::transport::Server::builder()
176            .accept_http1(true)
177            .max_connection_age(self.grpc_options.max_connection_age)
178            .timeout(self.grpc_options.request_timeout)
179            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
180            .layer(
181                TraceLayer::new(SharedClassifier::new(
182                    GrpcErrorsAsFailures::new()
183                        .with_success(GrpcCode::InvalidArgument)
184                        .with_success(GrpcCode::NotFound)
185                        .with_success(GrpcCode::ResourceExhausted)
186                        .with_success(GrpcCode::Unimplemented)
187                        .with_success(GrpcCode::Unknown),
188                ))
189                .make_span_with(grpc_trace_fn),
190            )
191            .layer(HealthCheckLayer)
192            .layer(cors_for_grpc_web_layer())
193            // Note: must wrap the accept/rate-limit layers so grpc-web callers receive
194            // grpc-web-compatible error responses instead of opaque transport failures.
195            .layer(GrpcWebLayer::new())
196            .layer(grpc::rate_limit_concurrent_connections(self.grpc_options))
197            .layer(grpc::rate_limit_per_ip(self.grpc_options)?)
198            // Resolve the (load-balancer-aware) client IP once here so handlers can read it from
199            // request extensions instead of re-deriving it from headers.
200            .layer(grpc::ResolveClientIpLayer)
201            // Note: must come after the CORS layer, as otherwise accept rejections do _not_ get
202            // CORS headers applied, masking the accept error in web-clients (which would experience
203            // CORS rejection).
204            .layer(
205                AcceptHeaderLayer::new(&rpc_version, genesis.commitment())
206                    .with_genesis_enforced_method("SubmitProvenTx")
207                    .with_genesis_enforced_method("SubmitProvenTxBatch"),
208            )
209            .add_service(api_service)
210            .add_service(health_service)
211            // Enables gRPC reflection service.
212            .add_service(reflection_service)
213            .serve_with_incoming_shutdown(
214                TcpListenerStream::new(self.listener),
215                shutdown.clone().cancelled_owned(),
216            );
217        tasks.spawn("RPC server", async move { rpc.await.map_err(|e| anyhow::anyhow!(e)) });
218
219        tasks.join_next_or_cancelled(shutdown).await
220    }
221}
222
223// INTERNAL SEQUENCER
224// ================================================================================================
225
226/// The internal Sequencer server.
227///
228/// Serves the private `sequencer.Api` gRPC service, which accepts already-authenticated
229/// transactions from full nodes and submits them directly to the mempool *without*
230/// re-verification.
231///
232/// This must only ever be exposed on a private, network-isolated listener: callers can inject
233/// transactions that the sequencer will not independently verify.
234pub struct SequencerInternal {
235    /// The listener the service binds to.
236    pub listener: TcpListener,
237    /// The in-process block producer API submissions are forwarded to.
238    pub block_producer: BlockProducerApi,
239    /// gRPC server options for internal services (timeouts).
240    pub grpc_options: GrpcOptionsInternal,
241}
242
243impl SequencerInternal {
244    /// Serves the internal sequencer API.
245    ///
246    /// Executes in place (i.e. not spawned) and will run indefinitely until a fatal error is
247    /// encountered.
248    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
249        info!(target: COMPONENT, endpoint = ?self.listener, "Internal sequencer server initialized");
250
251        let service = SequencerInternalService { block_producer: self.block_producer };
252
253        // Note: deliberately no accept-header / rate-limit / auth layers; this is a private,
254        // trusted interface and is expected to be network-isolated.
255        tonic::transport::Server::builder()
256            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
257            .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn))
258            .timeout(self.grpc_options.request_timeout)
259            .add_service(sequencer_api::service(service))
260            .serve_with_incoming_shutdown(
261                TcpListenerStream::new(self.listener),
262                shutdown.cancelled_owned(),
263            )
264            .await
265            .context("failed to serve internal sequencer API")
266    }
267}