Skip to main content

thruput_node_supervisor/
server.rs

1//! Top-level node supervisor orchestration.
2//!
3//! Owns hogd-dev process supervision and the public JSON-RPC server. Backend
4//! adapters land by plane: chain and submit are backed by hogd-dev's
5//! `SchedGateway` endpoint; receipt/event reads are backed by hogd-dev's
6//! `IndexerBlockExplorer` endpoint when advertised in readiness, and
7//! latest-state storage reads can be backed by hogd-dev's `DbGateway`
8//! endpoint independently.
9
10use std::{future::Future, io, net::SocketAddr, process::ExitStatus};
11
12use jsonrpsee::server::{Server, ServerConfig, ServerHandle};
13use serde::Serialize;
14use thruput_bundle_identity::BundleIdentity;
15use thruput_json_rpc_server::batch::{ConcurrentBatchService, MAX_RPC_BATCH_REQUESTS};
16use thruput_json_rpc_server::{
17    RpcServerConfig,
18    native::{
19        read::{NativeBlockPlane, NativeHistoryPlane, NativeSimulationPlane, NativeStatePlane},
20        service::{NativeCapabilities, NativeService},
21        transaction::NativeTransactionReadPlane,
22    },
23};
24use thruput_json_rpc_types::protocol_token::protocol_text;
25use thruput_json_rpc_types::{NetworkHash, network_info::NetworkInfo};
26use thruput_xdr::PROTOCOL_VERSION;
27
28use crate::{
29    BlockDriverConfig, HogdDevProcess, HogdDevReadiness, NodeSupervisorConfig, NodeSupervisorError,
30    airdrop::{LocalAirdropContext, LocalAirdropRpc},
31    block_driver::{BlockDriverTask, ManualBlockProducer},
32    hogd_dev::{DEFAULT_HOGD_DEV_BINARY, validate_runtime_identity},
33    read_backend::{HogdDevReadBackend, HogdDevStorageReadBackend},
34    rpc_backend::HogdDevSchedulerBackend,
35    stub_backend::StubBackend,
36    view_backend::HogdDevViewBackend,
37};
38
39/// Why a [`NodeSupervisorHandle::stopped`] call returned.
40///
41/// Callers (CLI exit codes, supervisors-of-supervisors) need to distinguish
42/// "hogd-dev died on us" from "we were asked to stop and hogd-dev is now
43/// down because we asked it to". Both branches return an `ExitStatus`, but
44/// the meaning is different.
45#[derive(Debug)]
46#[non_exhaustive]
47pub enum SupervisorStop {
48    /// hogd-dev exited on its own; the supervisor reacted by stopping the
49    /// JSON-RPC server. The contained status is hogd-dev's own exit status.
50    HogdExited(ExitStatus),
51    /// The JSON-RPC server stopped (typically via an external `stop()`
52    /// call), and the supervisor then shut hogd-dev down with the
53    /// configured grace timeout. The contained status is hogd-dev's exit
54    /// status from that shutdown.
55    RpcStopped(ExitStatus),
56    /// An external stop signal was received, and the supervisor then shut
57    /// hogd-dev down with the configured grace timeout. The contained status
58    /// is hogd-dev's exit status from that requested shutdown.
59    StopRequested(ExitStatus),
60}
61
62/// Backend implementation currently serving a public RPC plane.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum PlaneStatus {
67    /// The public plane is backed by hogd-dev's `SchedGateway` x23 endpoint.
68    /// The serialized value remains `scheduler` for compatibility.
69    Scheduler,
70    /// The plane is backed by hogd-dev's `IndexerBlockExplorer` x23 endpoint.
71    Indexer,
72    /// The plane is backed by hogd-dev's `DbGateway` x23 endpoint.
73    DbGateway,
74    /// The plane is backed by hogd-dev's `ViewGateway` x23 endpoint.
75    ViewGateway,
76    /// The plane is intentionally served by the supervisor stub.
77    Stub,
78}
79
80impl PlaneStatus {
81    /// Returns true when this plane is backed by hogd-dev's `IndexerBlockExplorer` endpoint.
82    pub const fn is_indexer_backed(self) -> bool {
83        match self {
84            Self::Indexer => true,
85            Self::Scheduler | Self::DbGateway | Self::ViewGateway | Self::Stub => false,
86        }
87    }
88}
89
90/// Backend plane status advertised by the node supervisor.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
92pub struct BackendPlanes {
93    /// Transaction submission plane.
94    pub submit: PlaneStatus,
95    /// Chain metadata plane.
96    pub chain: PlaneStatus,
97    /// Committed-state read plane.
98    pub read: PlaneStatus,
99    /// Read-only contract execution plane.
100    pub view: PlaneStatus,
101}
102
103impl BackendPlanes {
104    /// Scheduler-backed submission without DB or execution read endpoints.
105    pub const fn hogd_dev_scheduler_only() -> Self {
106        Self {
107            submit: PlaneStatus::Scheduler,
108            chain: PlaneStatus::Stub,
109            read: PlaneStatus::Stub,
110            view: PlaneStatus::Stub,
111        }
112    }
113
114    /// Scheduler submission, DB chain metadata, and indexed state reads.
115    pub const fn hogd_dev_full(view: PlaneStatus) -> Self {
116        Self {
117            submit: PlaneStatus::Scheduler,
118            chain: PlaneStatus::DbGateway,
119            read: PlaneStatus::Indexer,
120            view,
121        }
122    }
123
124    /// Scheduler submission with DB-backed chain metadata and storage reads.
125    pub const fn hogd_dev_storage_only(view: PlaneStatus) -> Self {
126        Self {
127            submit: PlaneStatus::Scheduler,
128            chain: PlaneStatus::DbGateway,
129            read: PlaneStatus::DbGateway,
130            view,
131        }
132    }
133}
134
135/// Running node supervisor.
136///
137/// Owns the supervised hogd-dev child and the JSON-RPC server. The handle
138/// does no proactive supervision in the background: callers are responsible
139/// for awaiting [`NodeSupervisorHandle::stopped`] (the supervisor-level
140/// "run until something ends" entry point) or driving [`NodeSupervisorHandle::stop`]
141/// directly. Both consume the handle so a single instance cannot be torn
142/// down twice.
143#[derive(Debug)]
144pub struct NodeSupervisorHandle {
145    hogd: HogdDevProcess,
146    rpc_handle: ServerHandle,
147    rpc_addr: SocketAddr,
148    backend_planes: BackendPlanes,
149    manual_block_producer: Option<ManualBlockProducer>,
150    block_driver: Option<BlockDriverTask>,
151}
152
153impl NodeSupervisorHandle {
154    /// Spawn hogd-dev, wait for readiness, connect the scheduler x23 client,
155    /// and start the JSON-RPC server.
156    ///
157    /// On any failure after hogd-dev has started, the child is stopped before
158    /// the error is returned, so a failed `start` does not leave a hogd-dev
159    /// process behind.
160    #[tracing::instrument(skip_all, fields(rpc_listen = %config.rpc_listen_address))]
161    pub async fn start(config: NodeSupervisorConfig) -> Result<Self, NodeSupervisorError> {
162        Self::start_with_inherited_file(config, None).await
163    }
164
165    /// Start a node with one inherited file descriptor, such as a locked bundle
166    /// lease. The child and its descendants retain the open file description
167    /// independently of this handle and process, including after SIGKILL.
168    /// The executable must preserve inherited descriptors for this guarantee.
169    pub async fn start_with_inherited_file(
170        config: NodeSupervisorConfig,
171        inherited_file: Option<std::fs::File>,
172    ) -> Result<Self, NodeSupervisorError> {
173        if let Some(identity) = &config.bundle_identity {
174            tracing::info!(
175                component_version = identity.build_version().crate_version(),
176                source_commit = ?identity.build_version().source_commit(),
177                release_version = ?identity.release_version(),
178                bundle_manifest_digest = ?identity.bundle_manifest_digest(),
179                bundle_manifest_mismatch_reason = ?identity.mismatch_reason(),
180                "Thruput release identity",
181            );
182        }
183        config
184            .local_airdrop
185            .validate()
186            .map_err(|reason| NodeSupervisorError::InvalidLocalAirdropConfig { reason })?;
187        let mut hogd =
188            HogdDevProcess::start_with_inherited_file(&config.hogd_dev, inherited_file).await?;
189
190        // Text, not the wire array: this is checked against the runtime
191        // manifest's `protocol` JSON field and carried in error messages.
192        let default_protocol = protocol_text(&PROTOCOL_VERSION);
193        let protocol_version = config
194            .hogd_protocol_override
195            .as_deref()
196            .unwrap_or(&default_protocol);
197
198        match validate_runtime_identity(
199            hogd.readiness(),
200            protocol_version,
201            hogd.expected_network_hash(),
202        ) {
203            Ok(()) => {}
204            Err(error) => {
205                if let Err(stop_err) = hogd.stop().await {
206                    tracing::warn!(
207                        error = %stop_err,
208                        "failed to stop hogd-dev after readiness validation error",
209                    );
210                }
211                return Err(error);
212            }
213        }
214
215        let started = async {
216            start_rpc_server(
217                &config,
218                hogd.readiness(),
219                protocol_version,
220                hogd.faucet_signer()?,
221                hogd.funding_journal()?,
222                hogd.funding_lock_lease(),
223            )
224            .await
225        }
226        .await;
227        match started {
228            Ok(started) => {
229                tracing::info!(rpc_addr = %started.addr, "node supervisor JSON-RPC ready");
230                Ok(Self {
231                    hogd,
232                    rpc_handle: started.handle,
233                    rpc_addr: started.addr,
234                    backend_planes: started.backend_planes,
235                    manual_block_producer: started.block_driver.manual_producer(),
236                    block_driver: Some(started.block_driver),
237                })
238            }
239            Err(error) => {
240                // Roll back the hogd-dev child so a failed RPC bind does not
241                // leak a child process.
242                if let Err(stop_err) = hogd.stop().await {
243                    tracing::warn!(
244                        error = %stop_err,
245                        "failed to stop hogd-dev after JSON-RPC startup error",
246                    );
247                }
248                Err(error)
249            }
250        }
251    }
252
253    /// Bound JSON-RPC listen address. Differs from the configured address when
254    /// the caller asked for port `0`.
255    pub fn rpc_addr(&self) -> SocketAddr {
256        self.rpc_addr
257    }
258
259    /// Parsed hogd-dev readiness payload.
260    pub fn hogd_readiness(&self) -> &HogdDevReadiness {
261        self.hogd.readiness()
262    }
263
264    /// Backend plane status for startup reporting.
265    pub fn backend_planes(&self) -> BackendPlanes {
266        self.backend_planes
267    }
268
269    /// Operating-system process id for hogd-dev, if available.
270    pub fn hogd_child_id(&self) -> Option<u32> {
271        self.hogd.child_id()
272    }
273
274    /// In-process block production control when the supervisor runs in manual mode.
275    pub fn manual_block_producer(&self) -> Option<ManualBlockProducer> {
276        self.manual_block_producer.clone()
277    }
278
279    /// Wait until either hogd-dev exits or the JSON-RPC server stops, then
280    /// tear the other side down.
281    ///
282    /// If hogd-dev dies after readiness, the public JSON-RPC facade keeps
283    /// answering but every backend call would fail; this method observes
284    /// that and shuts the facade down so the CLI sees a clean stop instead
285    /// of silent malfunction. If the JSON-RPC server stops first (e.g. via
286    /// an external [`NodeSupervisorHandle::stop`] call), hogd-dev is shut
287    /// down with the configured grace timeout.
288    ///
289    /// Returns a [`SupervisorStop`] describing which side ended first.
290    #[tracing::instrument(skip_all)]
291    pub async fn stopped(mut self) -> Result<SupervisorStop, NodeSupervisorError> {
292        self.run_until_inner(std::future::pending::<Result<(), NodeSupervisorError>>())
293            .await
294    }
295
296    /// Wait until hogd-dev exits, the JSON-RPC server stops, or an external
297    /// stop signal completes, then tear the remaining side down.
298    ///
299    /// This is the CLI-friendly variant of [`NodeSupervisorHandle::stopped`]:
300    /// callers pass a future for Ctrl-C or another stop request without
301    /// losing the ability to shut hogd-dev down cleanly.
302    #[tracing::instrument(skip_all)]
303    pub async fn run_until<F>(
304        mut self,
305        stop_signal: F,
306    ) -> Result<SupervisorStop, NodeSupervisorError>
307    where
308        F: Future<Output = Result<(), NodeSupervisorError>>,
309    {
310        self.run_until_inner(stop_signal).await
311    }
312
313    async fn run_until_inner<F>(
314        &mut self,
315        stop_signal: F,
316    ) -> Result<SupervisorStop, NodeSupervisorError>
317    where
318        F: Future<Output = Result<(), NodeSupervisorError>>,
319    {
320        let rpc_done = self.rpc_handle.clone().stopped();
321        tokio::select! {
322            result = self.hogd.stopped() => {
323                match &result {
324                    Ok(status) => tracing::warn!(?status, "hogd-dev exited; stopping JSON-RPC server"),
325                    Err(error) => tracing::error!(%error, "hog-devnode cleanup failed; stopping JSON-RPC server"),
326                }
327                if let Err(error) = self.rpc_handle.stop() {
328                    tracing::debug!(%error, "JSON-RPC server already stopped");
329                }
330                self.stop_block_driver().await;
331                self.rpc_handle.clone().stopped().await;
332                result.map(SupervisorStop::HogdExited)
333            }
334            () = rpc_done => {
335                tracing::info!("JSON-RPC server stopped; stopping hogd-dev");
336                let status = self.stop_inner().await?;
337                Ok(SupervisorStop::RpcStopped(status))
338            }
339            result = stop_signal => {
340                if let Err(error) = result {
341                    tracing::warn!(%error, "stop signal failed; stopping node supervisor");
342                    if let Err(stop_error) = self.stop_inner().await {
343                        tracing::error!(%stop_error, "failed to stop node supervisor after stop signal error");
344                    }
345                    return Err(error);
346                }
347                tracing::info!("stop signal received; stopping node supervisor");
348                let status = self.stop_inner().await?;
349                Ok(SupervisorStop::StopRequested(status))
350            }
351        }
352    }
353
354    /// Stop the JSON-RPC server and shut hogd-dev down with the configured
355    /// grace timeout. Consumes the handle.
356    #[tracing::instrument(skip_all)]
357    pub async fn stop(mut self) -> Result<ExitStatus, NodeSupervisorError> {
358        self.stop_inner().await
359    }
360
361    async fn stop_inner(&mut self) -> Result<ExitStatus, NodeSupervisorError> {
362        if let Err(error) = self.rpc_handle.stop() {
363            tracing::debug!(%error, "JSON-RPC server already stopped");
364        }
365        self.stop_block_driver().await;
366        self.rpc_handle.clone().stopped().await;
367        self.hogd.stop().await
368    }
369
370    async fn stop_block_driver(&mut self) {
371        if let Some(block_driver) = self.block_driver.take() {
372            block_driver.stop().await;
373        }
374    }
375}
376
377async fn start_rpc_server(
378    config: &NodeSupervisorConfig,
379    readiness: &HogdDevReadiness,
380    protocol_version: &str,
381    signer: std::sync::Arc<thruput_signing::LocalSigner>,
382    journal: std::sync::Arc<std::sync::Mutex<crate::funding_journal::FundingJournal>>,
383    lock_lease: Option<std::sync::Arc<std::fs::File>>,
384) -> Result<StartedRpcServer, NodeSupervisorError> {
385    let listen_address = &config.rpc_listen_address;
386    let manifest = readiness.runtime_manifest.as_ref().ok_or_else(|| {
387        NodeSupervisorError::InvalidRuntimeManifest {
388            program: DEFAULT_HOGD_DEV_BINARY.to_owned(),
389            reason: "hog-devnode readiness must advertise a runtime manifest".to_owned(),
390        }
391    })?;
392    let network_hash = manifest.network_hash.to_network_hash().into_inner();
393    let network_info = NetworkInfo::new(NetworkHash::from(network_hash));
394    let scheduler_backend = connect_scheduler_backend(readiness, protocol_version).await?;
395
396    let view = HogdDevViewBackend::connect_optional(
397        readiness.endpoints.view_gateway,
398        protocol_version,
399        readiness.protocol.as_deref(),
400        config.view_timeout,
401    )
402    .await?;
403    let view_status = view.plane_status();
404    let db_gateway = readiness.endpoints.db_gateway;
405
406    // Prefer the full indexer-backed read plane when available. If hogd-dev
407    // only advertises DbGateway, still serve latest-state storage reads.
408    if let Some(indexer_addr) = readiness.endpoints.indexer_block_explorer {
409        let local_airdrop =
410            validated_local_airdrop_context(manifest, config, signer, journal, lock_lease)?;
411        tracing::debug!(%indexer_addr, "connecting to hogd-dev IndexerBlockExplorer");
412        let read = HogdDevReadBackend::connect(
413            indexer_addr,
414            db_gateway,
415            scheduler_backend.scheduler_gateway_client().clone(),
416            protocol_version,
417            readiness.protocol.as_deref(),
418        )
419        .await?;
420        start_rpc_server_with_read(
421            listen_address,
422            scheduler_backend.with_db_gateway(
423                read.db_gateway_client().cloned(),
424                readiness.network_hash.clone(),
425            ),
426            read,
427            view,
428            rpc_server_options(
429                config,
430                network_info,
431                Some(local_airdrop),
432                BackendPlanes {
433                    chain: db_gateway.map_or(PlaneStatus::Stub, |_| PlaneStatus::DbGateway),
434                    ..BackendPlanes::hogd_dev_full(view_status)
435                },
436            ),
437        )
438        .await
439    } else if let Some(db_gateway_addr) = db_gateway {
440        let read = HogdDevStorageReadBackend::connect(
441            db_gateway_addr,
442            protocol_version,
443            readiness.protocol.as_deref(),
444        )
445        .await?;
446        start_rpc_server_with_read(
447            listen_address,
448            scheduler_backend.with_db_gateway(
449                Some(read.db_gateway_client().clone()),
450                readiness.network_hash.clone(),
451            ),
452            read,
453            view,
454            rpc_server_options(
455                config,
456                network_info,
457                None,
458                BackendPlanes::hogd_dev_storage_only(view_status),
459            ),
460        )
461        .await
462    } else {
463        tracing::warn!(
464            "hogd-dev did not advertise indexer or database endpoints; dependent RPC methods are unavailable"
465        );
466        start_rpc_server_with_read(
467            listen_address,
468            scheduler_backend,
469            StubBackend::new(),
470            view,
471            rpc_server_options(
472                config,
473                network_info,
474                None,
475                BackendPlanes {
476                    view: view_status,
477                    ..BackendPlanes::hogd_dev_scheduler_only()
478                },
479            ),
480        )
481        .await
482    }
483}
484
485async fn connect_scheduler_backend(
486    readiness: &HogdDevReadiness,
487    protocol_version: &str,
488) -> Result<HogdDevSchedulerBackend, NodeSupervisorError> {
489    let scheduler_gateway = readiness.endpoints.scheduler_gateway.ok_or_else(|| {
490        NodeSupervisorError::InvalidRuntimeManifest {
491            program: DEFAULT_HOGD_DEV_BINARY.to_owned(),
492            reason: "hog-devnode runtime manifest is missing endpoints.schedulerGateway".to_owned(),
493        }
494    })?;
495    let scheduler_backend = HogdDevSchedulerBackend::connect(
496        readiness.endpoints.scheduler_coord,
497        scheduler_gateway,
498        protocol_version,
499        readiness.protocol.as_deref(),
500    )
501    .await?;
502    scheduler_backend
503        .require_dev_genesis(&readiness.network_hash)
504        .await?;
505    Ok(scheduler_backend)
506}
507
508fn rpc_server_options(
509    config: &NodeSupervisorConfig,
510    network_info: NetworkInfo,
511    local_airdrop: Option<LocalAirdropContext>,
512    backend_planes: BackendPlanes,
513) -> RpcServerOptions {
514    RpcServerOptions {
515        network_info,
516        local_airdrop,
517        backend_planes,
518        view_gas_limit: config.view_gas_limit,
519        view_batch: config.view_batch,
520        view_timeout_ms: u64::try_from(config.view_timeout.as_millis()).unwrap_or(u64::MAX),
521        block_driver_config: config.block_driver,
522        rpc_max_connections: config.rpc_max_connections,
523        rpc_batch_concurrency: config.rpc_batch_concurrency,
524        bundle_identity: config.bundle_identity.clone(),
525    }
526}
527
528fn validated_local_airdrop_context(
529    manifest: &thruput_json_rpc_types::runtime_manifest::RuntimeManifest,
530    config: &NodeSupervisorConfig,
531    signer: std::sync::Arc<thruput_signing::LocalSigner>,
532    journal: std::sync::Arc<std::sync::Mutex<crate::funding_journal::FundingJournal>>,
533    lock_lease: Option<std::sync::Arc<std::fs::File>>,
534) -> Result<LocalAirdropContext, NodeSupervisorError> {
535    LocalAirdropContext::from_validated_runtime(manifest, config, signer, journal, lock_lease)
536        .map_err(|reason| NodeSupervisorError::InvalidRuntimeManifest {
537            program: DEFAULT_HOGD_DEV_BINARY.to_owned(),
538            reason,
539        })
540}
541
542struct RpcServerOptions {
543    view_batch: thruput_json_rpc_server::ViewBatchConfig,
544    view_timeout_ms: u64,
545    network_info: NetworkInfo,
546    local_airdrop: Option<LocalAirdropContext>,
547    backend_planes: BackendPlanes,
548    view_gas_limit: u64,
549    block_driver_config: BlockDriverConfig,
550    rpc_max_connections: u32,
551    rpc_batch_concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency,
552    bundle_identity: Option<BundleIdentity>,
553}
554
555async fn start_rpc_server_with_read<R, V>(
556    listen_address: &str,
557    scheduler_backend: HogdDevSchedulerBackend,
558    read: R,
559    view: V,
560    options: RpcServerOptions,
561) -> Result<StartedRpcServer, NodeSupervisorError>
562where
563    R: crate::airdrop::FundingStatusPlane
564        + NativeStatePlane
565        + NativeTransactionReadPlane
566        + NativeBlockPlane
567        + NativeHistoryPlane
568        + Clone,
569    V: NativeSimulationPlane,
570{
571    let RpcServerOptions {
572        view_batch,
573        view_timeout_ms,
574        network_info,
575        local_airdrop,
576        backend_planes,
577        view_gas_limit,
578        block_driver_config,
579        rpc_max_connections,
580        rpc_batch_concurrency,
581        bundle_identity,
582    } = options;
583    ensure_rpc_nofile_limit(rpc_max_connections)?;
584    let max_response_body_size = jsonrpsee::core::TEN_MB_SIZE_BYTES;
585    let server_config = ServerConfig::builder()
586        .max_connections(rpc_max_connections)
587        .max_response_body_size(max_response_body_size)
588        .set_batch_request_config(jsonrpsee::server::BatchRequestConfig::Limit(
589            MAX_RPC_BATCH_REQUESTS,
590        ))
591        .build();
592    let rpc_middleware =
593        jsonrpsee::core::middleware::RpcServiceBuilder::new().layer_fn(move |inner| {
594            ConcurrentBatchService::new(inner, max_response_body_size, rpc_batch_concurrency)
595        });
596    let server = Server::builder()
597        .set_config(server_config)
598        .set_rpc_middleware(rpc_middleware)
599        .build(listen_address)
600        .await
601        .map_err(|source| NodeSupervisorError::RpcBind {
602            address: listen_address.to_string(),
603            source,
604        })?;
605    let rpc_addr = server
606        .local_addr()
607        .map_err(|source| NodeSupervisorError::RpcLocalAddr { source })?;
608
609    let block_driver = BlockDriverTask::start(
610        scheduler_backend.scheduler_client().clone(),
611        block_driver_config,
612    );
613    let block_driver_trigger = block_driver.trigger();
614    let scheduler_backend = scheduler_backend.with_block_driver(block_driver_trigger.clone());
615    let local_airdrop = local_airdrop.map(|context| {
616        LocalAirdropRpc::new(
617            context,
618            scheduler_backend.clone(),
619            read.clone(),
620            block_driver_trigger,
621        )
622    });
623    let core_rpc = NativeService::new(
624        scheduler_backend.clone(),
625        scheduler_backend,
626        read,
627        view,
628        network_info.network_hash,
629        RpcServerConfig::default()
630            .with_view_gas_limit(view_gas_limit)
631            .with_view_timeout_ms(view_timeout_ms)
632            .with_view_batch(view_batch),
633        thruput_json_rpc_server::build_version!(),
634        NativeCapabilities::new(
635            true,
636            backend_planes.read.is_indexer_backed(),
637            matches!(backend_planes.chain, PlaneStatus::DbGateway),
638            matches!(backend_planes.view, PlaneStatus::ViewGateway),
639        ),
640    )
641    .with_local_pending_count();
642    let core_rpc = match bundle_identity.as_ref() {
643        Some(identity) => core_rpc.with_bundle_identity(identity),
644        None => core_rpc,
645    };
646    let module = match local_airdrop {
647        Some(funding) => core_rpc
648            .with_funding(funding)
649            .into_module()
650            .expect("bug: native methods register without conflicts")
651            .into_rpc()
652            .remove_context(),
653        None => core_rpc
654            .into_module()
655            .expect("bug: native methods register without conflicts")
656            .into_rpc()
657            .remove_context(),
658    };
659
660    let rpc_handle = server.start(module);
661    Ok(StartedRpcServer {
662        handle: rpc_handle,
663        addr: rpc_addr,
664        backend_planes,
665        block_driver,
666    })
667}
668
669const RPC_NOFILE_LIMIT_FLOOR: u64 = 4_096;
670const RPC_NOFILE_LIMIT_HEADROOM: u64 = 512;
671
672fn desired_rpc_nofile_limit(rpc_max_connections: u32) -> u64 {
673    u64::from(rpc_max_connections)
674        .saturating_add(RPC_NOFILE_LIMIT_HEADROOM)
675        .max(RPC_NOFILE_LIMIT_FLOOR)
676}
677
678#[cfg(unix)]
679fn ensure_rpc_nofile_limit(rpc_max_connections: u32) -> Result<(), NodeSupervisorError> {
680    let target = desired_rpc_nofile_limit(rpc_max_connections);
681    let mut limits = libc::rlimit {
682        rlim_cur: 0,
683        rlim_max: 0,
684    };
685    let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut limits) };
686    if rc != 0 {
687        return Err(NodeSupervisorError::RpcNofileLimit {
688            target,
689            source: io::Error::last_os_error(),
690        });
691    }
692    let Some(new_soft) = rpc_nofile_soft_limit_update(target, limits)? else {
693        return Ok(());
694    };
695
696    let updated = libc::rlimit {
697        rlim_cur: new_soft,
698        rlim_max: limits.rlim_max,
699    };
700    let rc = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const updated) };
701    if rc != 0 {
702        return Err(NodeSupervisorError::RpcNofileLimit {
703            target,
704            source: io::Error::last_os_error(),
705        });
706    }
707    tracing::debug!(
708        previous = limits.rlim_cur,
709        current = new_soft,
710        hard = limits.rlim_max,
711        target,
712        "raised JSON-RPC nofile soft limit"
713    );
714    Ok(())
715}
716
717#[cfg(unix)]
718fn rpc_nofile_soft_limit_update(
719    target: u64,
720    limits: libc::rlimit,
721) -> Result<Option<libc::rlim_t>, NodeSupervisorError> {
722    let target_rlim = libc::rlim_t::try_from(target).unwrap_or(libc::RLIM_INFINITY);
723    if limits.rlim_cur >= target_rlim {
724        return Ok(None);
725    }
726    if limits.rlim_max != libc::RLIM_INFINITY && limits.rlim_max < target_rlim {
727        return Err(NodeSupervisorError::RpcNofileHardLimit {
728            target,
729            hard: limits.rlim_max,
730        });
731    }
732
733    if limits.rlim_max == libc::RLIM_INFINITY {
734        Ok(Some(target_rlim))
735    } else {
736        Ok(Some(target_rlim.min(limits.rlim_max)))
737    }
738}
739
740#[cfg(not(unix))]
741fn ensure_rpc_nofile_limit(_rpc_max_connections: u32) -> Result<(), NodeSupervisorError> {
742    Ok(())
743}
744
745#[derive(Debug)]
746struct StartedRpcServer {
747    handle: ServerHandle,
748    addr: SocketAddr,
749    backend_planes: BackendPlanes,
750    block_driver: BlockDriverTask,
751}
752
753#[cfg(test)]
754mod tests;