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