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::{Finality, State};
17use miden_node_utils::clap::{GrpcOptionsExternal, GrpcOptionsInternal};
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 store: Arc<State>,
49    pub mode: RpcMode,
50    pub ntx_builder: Option<NtxBuilderClient>,
51    pub grpc_options: GrpcOptionsExternal,
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#[derive(Clone, Debug)]
60pub enum RpcMode {
61    /// Sequencer RPC validates submissions locally, re-executes them through every validator, then
62    /// forwards them to the block producer.
63    ///
64    /// Every validator must observe every transaction: a validator only signs blocks whose
65    /// transactions it has previously validated, so a submission that misses a validator would
66    /// later prevent that validator from signing the block containing it.
67    Sequencer {
68        block_producer: Box<BlockProducerApi>,
69        validators: ValidatorClients,
70    },
71    /// Full-node RPC.
72    ///
73    /// By default it forwards submissions verbatim to the source RPC (the caller is responsible for
74    /// configuring this client with any request metadata the source RPC requires).
75    ///
76    /// When the pre-authenticated submission clients are set, the full-node will, instead of
77    /// forwarding, re-execute submissions through every validator and authenticate them against its
78    /// store, then submit the authenticated result directly to the sequencer's internal API.
79    FullNode {
80        source_rpc: Box<SourceRpcClient>,
81        readiness_threshold: u32,
82        pre_auth: Option<PreAuthSubmission>,
83    },
84}
85
86/// A non-empty set of validator clients.
87///
88/// Every submission is re-executed through every validator, and state shared by the validator set
89/// (such as the transaction encryption key) can be served by any single member, so an empty set is
90/// rejected at construction.
91#[derive(Clone, Debug)]
92pub struct ValidatorClients(Vec<ValidatorClient>);
93
94impl ValidatorClients {
95    /// # Errors
96    ///
97    /// Fails if `validators` is empty.
98    pub fn new(validators: Vec<ValidatorClient>) -> anyhow::Result<Self> {
99        anyhow::ensure!(!validators.is_empty(), "at least one validator is required");
100        Ok(Self(validators))
101    }
102
103    /// Returns a randomly chosen validator; use for state that any single validator can serve, so
104    /// the load spreads across the set.
105    pub(crate) fn random(&self) -> &ValidatorClient {
106        let index = rand::rng().random_range(0..self.0.len());
107        &self.0[index]
108    }
109
110    pub(crate) fn as_slice(&self) -> &[ValidatorClient] {
111        &self.0
112    }
113}
114
115/// Validator and sequencer clients for the full-node pre-authenticated submission path.
116///
117/// The two are only meaningful together: submissions are re-executed through every validator and
118/// the authenticated result is submitted to the sequencer's internal API, so a full node is
119/// configured with both or neither.
120#[derive(Clone, Debug)]
121pub struct PreAuthSubmission {
122    validators: ValidatorClients,
123    sequencer: Box<SequencerClient>,
124}
125
126impl PreAuthSubmission {
127    /// # Errors
128    ///
129    /// Fails if `validators` is empty; every submission must be re-executed by the validator set.
130    pub fn new(
131        validators: Vec<ValidatorClient>,
132        sequencer: SequencerClient,
133    ) -> anyhow::Result<Self> {
134        let validators = ValidatorClients::new(validators)
135            .context("pre-authenticated submission requires at least one validator")?;
136        Ok(Self {
137            validators,
138            sequencer: Box::new(sequencer),
139        })
140    }
141
142    pub(crate) fn validators(&self) -> &ValidatorClients {
143        &self.validators
144    }
145
146    pub(crate) fn sequencer(&self) -> &SequencerClient {
147        &self.sequencer
148    }
149}
150
151impl RpcMode {
152    pub fn sequencer(block_producer: BlockProducerApi, validators: ValidatorClients) -> Self {
153        Self::Sequencer {
154            block_producer: Box::new(block_producer),
155            validators,
156        }
157    }
158
159    pub fn full_node(
160        source_rpc: SourceRpcClient,
161        readiness_threshold: u32,
162        pre_auth: Option<PreAuthSubmission>,
163    ) -> Self {
164        Self::FullNode {
165            source_rpc: Box::new(source_rpc),
166            readiness_threshold,
167            pre_auth,
168        }
169    }
170
171    const fn as_str(&self) -> &'static str {
172        match self {
173            Self::Sequencer { .. } => "sequencer",
174            Self::FullNode { .. } => "full",
175        }
176    }
177}
178
179impl Rpc {
180    /// Serves the RPC API.
181    ///
182    /// In full-node mode, also runs the block/proof sync loop concurrently. Either component
183    /// failing causes both to stop.
184    ///
185    /// Note: Executes in place (i.e. not spawned) and will run indefinitely until
186    ///       a fatal error is encountered.
187    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
188        let endpoint = self.listener.local_addr().context("failed to read RPC listen address")?;
189        let mode = self.mode.as_str();
190        let mut api = api::RpcService::new(
191            self.store.clone(),
192            self.mode.clone(),
193            self.ntx_builder.clone(),
194            NonZeroUsize::new(1_000_000).unwrap(),
195            self.network_tx_auth.map(NetworkTxAuth),
196        );
197
198        let genesis = api
199            .get_genesis_header_with_retry()
200            .await
201            .context("Fetching genesis header from store")?;
202
203        api.set_genesis_commitment(genesis.commitment())?;
204
205        let api_service = rpc_api::service(api);
206
207        let mut tasks = Tasks::new();
208
209        // Initialize health reporter and sync service based on the RPC mode.
210        let (health_reporter, health_service) = tonic_health::server::health_reporter();
211        match self.mode {
212            RpcMode::Sequencer { .. } => {
213                health_reporter
214                    .set_service_status(
215                        rpc_api::service_name(),
216                        tonic_health::ServingStatus::Serving,
217                    )
218                    .await;
219                let chain_tip = self.store.chain_tip(Finality::Committed).await;
220                log_node_ready(mode, endpoint, chain_tip);
221            },
222            RpcMode::FullNode { source_rpc, readiness_threshold, .. } => {
223                health_reporter
224                    .set_service_status(
225                        rpc_api::service_name(),
226                        tonic_health::ServingStatus::NotServing,
227                    )
228                    .await;
229                let readiness = RpcReadiness::new(health_reporter, readiness_threshold);
230                tasks.spawn(
231                    "RPC sync",
232                    RpcSync {
233                        state: Arc::clone(&self.store),
234                        source_rpc: *source_rpc,
235                        readiness,
236                    }
237                    .run(shutdown.clone()),
238                );
239                log_node_synchronizing(mode, endpoint, readiness_threshold);
240            },
241        }
242
243        let reflection_service = server::Builder::configure()
244            .register_file_descriptor_set(rpc_api_descriptor())
245            .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
246            .build_v1()
247            .context("failed to build reflection service")?;
248
249        let rpc_version = env!("CARGO_PKG_VERSION");
250        let rpc_version =
251            semver::Version::parse(rpc_version).context("failed to parse crate version")?;
252
253        let rpc = tonic::transport::Server::builder()
254            .accept_http1(true)
255            .max_connection_age(self.grpc_options.max_connection_age)
256            .max_connection_age_grace(self.grpc_options.max_connection_age_grace)
257            .timeout(self.grpc_options.request_timeout)
258            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
259            .layer(
260                TraceLayer::new(SharedClassifier::new(
261                    GrpcErrorsAsFailures::new()
262                        .with_success(GrpcCode::InvalidArgument)
263                        .with_success(GrpcCode::NotFound)
264                        .with_success(GrpcCode::ResourceExhausted)
265                        .with_success(GrpcCode::Unimplemented)
266                        .with_success(GrpcCode::Unknown),
267                ))
268                .make_span_with(grpc_trace_fn),
269            )
270            .layer(HealthCheckLayer)
271            .layer(cors_for_grpc_web_layer())
272            // Note: must wrap the accept/rate-limit layers so grpc-web callers receive
273            // grpc-web-compatible error responses instead of opaque transport failures.
274            .layer(GrpcWebLayer::new())
275            .layer(grpc::rate_limit_concurrent_connections(self.grpc_options))
276            .layer(grpc::rate_limit_per_ip(self.grpc_options)?)
277            // Resolve the (load-balancer-aware) client IP once here so handlers can read it from
278            // request extensions instead of re-deriving it from headers.
279            .layer(grpc::ResolveClientIpLayer)
280            // Note: must come after the CORS layer, as otherwise accept rejections do _not_ get
281            // CORS headers applied, masking the accept error in web-clients (which would experience
282            // CORS rejection).
283            .layer(
284                AcceptHeaderLayer::new(&rpc_version, genesis.commitment())
285                    .with_genesis_enforced_method("SubmitProvenTx")
286                    .with_genesis_enforced_method("SubmitProvenTxBatch"),
287            )
288            .add_service(api_service)
289            .add_service(health_service)
290            // Enables gRPC reflection service.
291            .add_service(reflection_service)
292            .serve_with_incoming_shutdown(
293                TcpListenerStream::new(self.listener),
294                shutdown.clone().cancelled_owned(),
295            );
296        tasks.spawn("RPC server", async move { rpc.await.map_err(|e| anyhow::anyhow!(e)) });
297
298        tasks.join_next_or_cancelled(shutdown).await
299    }
300}
301
302fn log_node_ready(mode: &str, endpoint: impl Display, chain_tip: impl Display) {
303    info!(
304        target: LOG_TARGET,
305        {
306            service.name = "miden-node",
307            service.version = env!("CARGO_PKG_VERSION"),
308            node.role = mode,
309            rpc.listen = %endpoint,
310            block.number = %chain_tip,
311        },
312        "Node ready",
313    );
314}
315
316fn log_node_synchronizing(mode: &str, endpoint: impl Display, readiness_threshold: u32) {
317    info!(
318        target: LOG_TARGET,
319        {
320            service.name = "miden-node",
321            service.version = env!("CARGO_PKG_VERSION"),
322            node.role = mode,
323            rpc.listen = %endpoint,
324            sync.ready_threshold = readiness_threshold,
325        },
326        "Node started; synchronizing",
327    );
328}
329
330// INTERNAL SEQUENCER
331// ================================================================================================
332
333/// The internal Sequencer server.
334///
335/// Serves the private `sequencer.Api` gRPC service, which accepts already-authenticated
336/// transactions from full nodes and submits them directly to the mempool *without*
337/// re-verification.
338///
339/// This must only ever be exposed on a private, network-isolated listener: callers can inject
340/// transactions that the sequencer will not independently verify.
341pub struct SequencerInternal {
342    /// The listener the service binds to.
343    pub listener: TcpListener,
344    /// The in-process block producer API submissions are forwarded to.
345    pub block_producer: BlockProducerApi,
346    /// gRPC server options for internal services (timeouts).
347    pub grpc_options: GrpcOptionsInternal,
348}
349
350impl SequencerInternal {
351    /// Serves the internal sequencer API.
352    ///
353    /// Executes in place (i.e. not spawned) and will run indefinitely until a fatal error is
354    /// encountered.
355    pub async fn serve(self, shutdown: CancellationToken) -> anyhow::Result<()> {
356        let endpoint = self
357            .listener
358            .local_addr()
359            .context("failed to read internal sequencer listen address")?;
360        info!(
361            target: LOG_TARGET,
362            { internal.listen = %endpoint },
363            "Internal sequencer server ready",
364        );
365
366        let service = SequencerInternalService { block_producer: self.block_producer };
367
368        // Note: deliberately no accept-header / rate-limit / auth layers; this is a private,
369        // trusted interface and is expected to be network-isolated.
370        tonic::transport::Server::builder()
371            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
372            .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn))
373            .timeout(self.grpc_options.request_timeout)
374            .add_service(sequencer_api::service(service))
375            .serve_with_incoming_shutdown(
376                TcpListenerStream::new(self.listener),
377                shutdown.cancelled_owned(),
378            )
379            .await
380            .context("failed to serve internal sequencer API")
381    }
382}