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