Skip to main content

miden_node_rpc/server/
mod.rs

1use std::fmt::Display;
2use std::num::NonZeroUsize;
3use std::sync::Arc;
4
5use accept::AcceptHeaderLayer;
6use anyhow::Context;
7use miden_node_block_producer::{BlockProducerApi, RpcReadiness, RpcSync};
8use miden_node_proto::clients::{
9    NtxBuilderClient,
10    RpcClient as SourceRpcClient,
11    SequencerClient,
12    ValidatorClient,
13};
14use miden_node_proto::server::{rpc_api, sequencer_api};
15use miden_node_proto_build::rpc_api_descriptor;
16use miden_node_store::state::{BlockWriter, ProofWriter, State};
17use miden_node_utils::clap::GrpcOptions;
18use miden_node_utils::cors::cors_for_grpc_web_layer;
19use miden_node_utils::grpc;
20use miden_node_utils::panic::{CatchPanicLayer, catch_panic_layer_fn};
21use miden_node_utils::shutdown::CancellationToken;
22use miden_node_utils::tasks::Tasks;
23use miden_node_utils::tracing::grpc::grpc_trace_fn;
24use rand::RngExt;
25use tokio::net::TcpListener;
26use tokio_stream::wrappers::TcpListenerStream;
27use tonic::metadata::AsciiMetadataValue;
28use tonic_reflection::server;
29use tonic_web::GrpcWebLayer;
30use tower_http::classify::{GrpcCode, GrpcErrorsAsFailures, SharedClassifier};
31use tower_http::trace::TraceLayer;
32use tracing::info;
33
34use crate::LOG_TARGET;
35use crate::server::api::SequencerInternalService;
36use crate::server::health::HealthCheckLayer;
37
38mod accept;
39pub(crate) mod api;
40mod health;
41
42/// The RPC server component.
43///
44/// On startup, binds to the provided listener and starts serving the RPC API.
45/// It uses the supplied store state and mode-specific submission handling.
46pub struct Rpc {
47    pub listener: TcpListener,
48    pub state: Arc<State>,
49    pub mode: RpcMode,
50    pub ntx_builder: Option<NtxBuilderClient>,
51    pub grpc_options: GrpcOptions,
52    pub network_tx_auth: Option<AsciiMetadataValue>,
53}
54
55#[derive(Clone, Debug)]
56/// Shared secret value expected in the fixed `x-miden-network-tx-auth` metadata header.
57pub(crate) struct NetworkTxAuth(pub(crate) AsciiMetadataValue);
58
59/// How the RPC is wired at startup: which submission path it runs and, in full-node mode, the
60/// store write capabilities its sync loop consumes.
61///
62/// Deliberately not `Clone`: the full-node variant owns the store's single-writer capabilities,
63/// which must not be duplicated. Per-request handlers never need those, so they read through
64/// [`RpcBackend`] instead — the `Clone`-able subset [`Self::backend`] derives from this.
65pub enum RpcMode {
66    /// Sequencer RPC validates submissions locally, re-executes them through every validator, then
67    /// forwards them to the block producer.
68    ///
69    /// Every validator must observe every transaction: a validator only signs blocks whose
70    /// transactions it has previously validated, so a submission that misses a validator would
71    /// later prevent that validator from signing the block containing it.
72    Sequencer {
73        block_producer: Box<BlockProducerApi>,
74        validators: ValidatorClients,
75    },
76    /// Full-node RPC.
77    ///
78    /// By default it forwards submissions verbatim to the source RPC (the caller is responsible for
79    /// configuring this client with any request metadata the source RPC requires).
80    ///
81    /// When the pre-authenticated submission clients are set, the full-node will, instead of
82    /// forwarding, re-execute submissions through every validator and authenticate them against its
83    /// store, then submit the authenticated result directly to the sequencer's internal API.
84    FullNode {
85        source_rpc: Box<SourceRpcClient>,
86        readiness_threshold: u32,
87        pre_auth: Option<PreAuthSubmission>,
88        /// The store's block-write capability, handed to the sync loop.
89        block_writer: BlockWriter,
90        /// The store's proof-write capability, handed to the sync loop.
91        proof_writer: ProofWriter,
92    },
93}
94
95/// The clients handlers forward requests to — the per-request subset of [`RpcMode`], held by
96/// [`RpcService`](api::RpcService) for the server's lifetime and read by every handler.
97///
98/// `Clone` because it is cloned once into `RpcService` and then read on every request; it never
99/// carries the full-node's store write capabilities ([`RpcMode`] does), since no handler needs
100/// them — those are consumed once by the sync loop at startup.
101#[derive(Clone, Debug)]
102pub(crate) enum RpcBackend {
103    Sequencer {
104        block_producer: Box<BlockProducerApi>,
105        validators: ValidatorClients,
106    },
107    FullNode {
108        source_rpc: Box<SourceRpcClient>,
109        pre_auth: Option<PreAuthSubmission>,
110    },
111}
112
113#[cfg(test)]
114impl RpcBackend {
115    /// Test-only: production code only ever builds a backend from an [`RpcMode`] via
116    /// [`RpcMode::backend`]; these let handler-level tests construct one directly, without a store
117    /// or write capabilities.
118    pub(crate) fn sequencer(
119        block_producer: BlockProducerApi,
120        validators: ValidatorClients,
121    ) -> Self {
122        Self::Sequencer {
123            block_producer: Box::new(block_producer),
124            validators,
125        }
126    }
127
128    pub(crate) fn full_node(
129        source_rpc: SourceRpcClient,
130        pre_auth: Option<PreAuthSubmission>,
131    ) -> Self {
132        Self::FullNode {
133            source_rpc: Box::new(source_rpc),
134            pre_auth,
135        }
136    }
137}
138
139/// A non-empty set of validator clients.
140///
141/// Every submission is re-executed through every validator, and state shared by the validator set
142/// (such as the transaction encryption key) can be served by any single member, so an empty set is
143/// rejected at construction.
144#[derive(Clone, Debug)]
145pub struct ValidatorClients(Vec<ValidatorClient>);
146
147impl ValidatorClients {
148    /// # Errors
149    ///
150    /// Fails if `validators` is empty.
151    pub fn new(validators: Vec<ValidatorClient>) -> anyhow::Result<Self> {
152        anyhow::ensure!(!validators.is_empty(), "at least one validator is required");
153        Ok(Self(validators))
154    }
155
156    /// Returns a randomly chosen validator; use for state that any single validator can serve, so
157    /// the load spreads across the set.
158    pub(crate) fn random(&self) -> &ValidatorClient {
159        let index = rand::rng().random_range(0..self.0.len());
160        &self.0[index]
161    }
162
163    pub(crate) fn as_slice(&self) -> &[ValidatorClient] {
164        &self.0
165    }
166}
167
168/// Validator and sequencer clients for the full-node pre-authenticated submission path.
169///
170/// The two are only meaningful together: submissions are re-executed through every validator and
171/// the authenticated result is submitted to the sequencer's internal API, so a full node is
172/// configured with both or neither.
173#[derive(Clone, Debug)]
174pub struct PreAuthSubmission {
175    validators: ValidatorClients,
176    sequencer: Box<SequencerClient>,
177}
178
179impl PreAuthSubmission {
180    /// # Errors
181    ///
182    /// Fails if `validators` is empty; every submission must be re-executed by the validator set.
183    pub fn new(
184        validators: Vec<ValidatorClient>,
185        sequencer: SequencerClient,
186    ) -> anyhow::Result<Self> {
187        let validators = ValidatorClients::new(validators)
188            .context("pre-authenticated submission requires at least one validator")?;
189        Ok(Self {
190            validators,
191            sequencer: Box::new(sequencer),
192        })
193    }
194
195    pub(crate) fn validators(&self) -> &ValidatorClients {
196        &self.validators
197    }
198
199    pub(crate) fn sequencer(&self) -> &SequencerClient {
200        &self.sequencer
201    }
202}
203
204impl RpcMode {
205    pub fn sequencer(block_producer: BlockProducerApi, validators: ValidatorClients) -> Self {
206        Self::Sequencer {
207            block_producer: Box::new(block_producer),
208            validators,
209        }
210    }
211
212    pub fn full_node(
213        source_rpc: SourceRpcClient,
214        readiness_threshold: u32,
215        pre_auth: Option<PreAuthSubmission>,
216        block_writer: BlockWriter,
217        proof_writer: ProofWriter,
218    ) -> Self {
219        Self::FullNode {
220            source_rpc: Box::new(source_rpc),
221            readiness_threshold,
222            pre_auth,
223            block_writer,
224            proof_writer,
225        }
226    }
227
228    const fn as_str(&self) -> &'static str {
229        match self {
230            Self::Sequencer { .. } => "sequencer",
231            Self::FullNode { .. } => "full",
232        }
233    }
234
235    /// Returns the `Clone`-able per-request backend subset handed to
236    /// [`RpcService`](api::RpcService).
237    fn backend(&self) -> RpcBackend {
238        match self {
239            Self::Sequencer { block_producer, validators } => RpcBackend::Sequencer {
240                block_producer: block_producer.clone(),
241                validators: validators.clone(),
242            },
243            Self::FullNode { source_rpc, pre_auth, .. } => RpcBackend::FullNode {
244                source_rpc: source_rpc.clone(),
245                pre_auth: pre_auth.clone(),
246            },
247        }
248    }
249}
250
251impl Rpc {
252    /// Serves the RPC API.
253    ///
254    /// In full-node mode, also runs the block/proof sync loop concurrently. Either component
255    /// failing causes both to stop.
256    ///
257    /// Note: Executes in place (i.e. not spawned) and will run indefinitely until
258    ///       a fatal error is encountered.
259    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
260        let endpoint = self.listener.local_addr().context("failed to read RPC listen address")?;
261        let mode = self.mode.as_str();
262        let mut api = api::RpcService::new(
263            self.state.clone(),
264            self.mode.backend(),
265            self.ntx_builder.clone(),
266            NonZeroUsize::new(1_000_000).unwrap(),
267            self.network_tx_auth.map(NetworkTxAuth),
268        );
269
270        let genesis = api
271            .get_genesis_header_with_retry()
272            .await
273            .context("Fetching genesis header from store")?;
274
275        api.set_genesis_commitment(genesis.commitment())?;
276
277        let api_service = rpc_api::service(api);
278
279        let mut tasks = Tasks::new();
280
281        // Initialize health reporter and sync service based on the RPC mode.
282        let (health_reporter, health_service) = tonic_health::server::health_reporter();
283        match self.mode {
284            RpcMode::Sequencer { .. } => {
285                health_reporter
286                    .set_service_status(
287                        rpc_api::service_name(),
288                        tonic_health::ServingStatus::Serving,
289                    )
290                    .await;
291                let chain_tip = self.state.committed_tip();
292                log_node_ready(mode, endpoint, chain_tip);
293            },
294            RpcMode::FullNode {
295                source_rpc,
296                readiness_threshold,
297                block_writer,
298                proof_writer,
299                ..
300            } => {
301                Self::spawn_full_node_sync(
302                    &self.state,
303                    &mut tasks,
304                    health_reporter,
305                    mode,
306                    endpoint,
307                    shutdown.clone(),
308                    *source_rpc,
309                    readiness_threshold,
310                    block_writer,
311                    proof_writer,
312                )
313                .await;
314            },
315        }
316
317        let reflection_service = server::Builder::configure()
318            .register_file_descriptor_set(rpc_api_descriptor())
319            .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
320            .build_v1()
321            .context("failed to build reflection service")?;
322
323        let rpc_version = env!("CARGO_PKG_VERSION");
324        let rpc_version =
325            semver::Version::parse(rpc_version).context("failed to parse crate version")?;
326
327        let rpc = tonic::transport::Server::builder()
328            .accept_http1(true)
329            .timeout(self.grpc_options.request_timeout)
330            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
331            .layer(
332                TraceLayer::new(SharedClassifier::new(
333                    GrpcErrorsAsFailures::new()
334                        .with_success(GrpcCode::InvalidArgument)
335                        .with_success(GrpcCode::NotFound)
336                        .with_success(GrpcCode::ResourceExhausted)
337                        .with_success(GrpcCode::Unimplemented)
338                        .with_success(GrpcCode::Unknown),
339                ))
340                .make_span_with(grpc_trace_fn),
341            )
342            .layer(HealthCheckLayer)
343            .layer(cors_for_grpc_web_layer())
344            // Note: must wrap the accept layer so grpc-web callers receive grpc-web-compatible
345            // error responses instead of opaque transport failures.
346            .layer(GrpcWebLayer::new())
347            // Resolve the (load-balancer-aware) client IP once here so handlers can read it from
348            // request extensions instead of re-deriving it from headers.
349            .layer(grpc::ResolveClientIpLayer)
350            // Note: must come after the CORS layer, as otherwise accept rejections do _not_ get
351            // CORS headers applied, masking the accept error in web-clients (which would experience
352            // CORS rejection).
353            .layer(
354                AcceptHeaderLayer::new(&rpc_version, genesis.commitment())
355                    .with_genesis_enforced_method("SubmitProvenTx")
356                    .with_genesis_enforced_method("SubmitProvenTxBatch"),
357            )
358            .add_service(api_service)
359            .add_service(health_service)
360            // Enables gRPC reflection service.
361            .add_service(reflection_service)
362            .serve_with_incoming_shutdown(
363                TcpListenerStream::new(self.listener),
364                shutdown.clone().cancelled_owned(),
365            );
366        tasks.spawn("RPC server", async move { rpc.await.map_err(|e| anyhow::anyhow!(e)) });
367
368        tasks.join_next_or_cancelled(shutdown).await
369    }
370
371    /// Marks the RPC `NotServing` until synchronized, then spawns the full-node sync loop.
372    #[expect(
373        clippy::too_many_arguments,
374        reason = "assembles the full-node sync task from Rpc::serve's local state"
375    )]
376    async fn spawn_full_node_sync(
377        state: &Arc<State>,
378        tasks: &mut Tasks,
379        health_reporter: tonic_health::server::HealthReporter,
380        mode: &str,
381        endpoint: impl Display,
382        shutdown: CancellationToken,
383        source_rpc: SourceRpcClient,
384        readiness_threshold: u32,
385        block_writer: BlockWriter,
386        proof_writer: ProofWriter,
387    ) {
388        health_reporter
389            .set_service_status(rpc_api::service_name(), tonic_health::ServingStatus::NotServing)
390            .await;
391        let readiness = RpcReadiness::new(health_reporter, readiness_threshold);
392        tasks.spawn(
393            "RPC sync",
394            RpcSync {
395                state: Arc::clone(state),
396                block_writer,
397                proof_writer,
398                source_rpc,
399                readiness,
400            }
401            .run(shutdown),
402        );
403        log_node_synchronizing(mode, endpoint, readiness_threshold);
404    }
405}
406
407fn log_node_ready(mode: &str, endpoint: impl Display, chain_tip: impl Display) {
408    info!(
409        target: LOG_TARGET,
410        {
411            service.name = "miden-node",
412            service.version = env!("CARGO_PKG_VERSION"),
413            node.role = mode,
414            rpc.listen = %endpoint,
415            block.number = %chain_tip,
416        },
417        "Node ready",
418    );
419}
420
421fn log_node_synchronizing(mode: &str, endpoint: impl Display, readiness_threshold: u32) {
422    info!(
423        target: LOG_TARGET,
424        {
425            service.name = "miden-node",
426            service.version = env!("CARGO_PKG_VERSION"),
427            node.role = mode,
428            rpc.listen = %endpoint,
429            sync.ready_threshold = readiness_threshold,
430        },
431        "Node started; synchronizing",
432    );
433}
434
435// INTERNAL SEQUENCER
436// ================================================================================================
437
438/// The internal Sequencer server.
439///
440/// Serves the private `sequencer.Api` gRPC service, which accepts already-authenticated
441/// transactions from full nodes and submits them directly to the mempool *without*
442/// re-verification.
443///
444/// This must only ever be exposed on a private, network-isolated listener: callers can inject
445/// transactions that the sequencer will not independently verify.
446pub struct SequencerInternal {
447    /// The listener the service binds to.
448    pub listener: TcpListener,
449    /// The in-process block producer API submissions are forwarded to.
450    pub block_producer: BlockProducerApi,
451    /// gRPC server options for internal services (timeouts).
452    pub grpc_options: GrpcOptions,
453}
454
455impl SequencerInternal {
456    /// Serves the internal sequencer API.
457    ///
458    /// Executes in place (i.e. not spawned) and will run indefinitely until a fatal error is
459    /// encountered.
460    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
461        let endpoint = self
462            .listener
463            .local_addr()
464            .context("failed to read internal sequencer listen address")?;
465        info!(
466            target: LOG_TARGET,
467            { internal.listen = %endpoint },
468            "Internal sequencer server ready",
469        );
470
471        let service = SequencerInternalService { block_producer: self.block_producer };
472
473        // Note: deliberately no accept-header / auth layers; this is a private, trusted interface
474        // and is expected to be network-isolated.
475        tonic::transport::Server::builder()
476            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
477            .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn))
478            .timeout(self.grpc_options.request_timeout)
479            .add_service(sequencer_api::service(service))
480            .serve_with_incoming_shutdown(
481                TcpListenerStream::new(self.listener),
482                shutdown.cancelled_owned(),
483            )
484            .await
485            .context("failed to serve internal sequencer API")
486    }
487}