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