Skip to main content

thruput_node_supervisor/
config.rs

1//! Configuration for the node supervisor.
2
3use std::{
4    collections::BTreeMap,
5    ffi::OsString,
6    path::{Path, PathBuf},
7    time::Duration,
8};
9
10use thruput_bundle_identity::BundleIdentity;
11use thruput_json_rpc_types::bootstrap_manifest::{
12    BootstrapManifest, BootstrapManifestAllocation, BootstrapManifestStorageCostModel,
13};
14use thruput_json_rpc_types::{Ed25519PublicKey, NetworkHashText};
15use thruput_tx_validation::ConfigParamName;
16
17/// Default JSON-RPC listen address for `thruput node`.
18pub const DEFAULT_RPC_LISTEN_ADDRESS: &str = "127.0.0.1:9545";
19
20/// Default maximum simultaneous public JSON-RPC connections for `thruput node`.
21pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 2_000;
22
23/// Default timeout for one hogd-dev `ViewGateway` execution RPC.
24pub const DEFAULT_HOGD_DEV_VIEW_TIMEOUT: Duration = Duration::from_secs(30);
25
26/// Default interval for coordinator-like local block production.
27pub const DEFAULT_BLOCK_DRIVER_INTERVAL: Duration = Duration::from_millis(500);
28
29/// Default deadline for one explicitly requested block-production cycle.
30pub const DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
31
32/// Default listen host passed to hogd-dev for internal services.
33pub const DEFAULT_HOGD_DEV_LISTEN_HOST: &str = "127.0.0.1";
34
35/// Default graceful shutdown timeout for ephemeral hogd-dev.
36pub const DEFAULT_HOGD_DEV_SHUTDOWN_GRACE: Duration = Duration::from_secs(5);
37
38/// Default graceful shutdown timeout for hogd-dev with persistent state.
39pub const DEFAULT_HOGD_DEV_PERSISTENT_SHUTDOWN_GRACE: Duration = Duration::from_secs(30);
40
41/// Default startup timeout while waiting for hogd-dev readiness.
42pub const DEFAULT_HOGD_DEV_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
43
44/// Fixed local faucet allocation in raw native token units.
45pub const LOCAL_FAUCET_STORAGE_TOKENS: u64 = 10_000_000_000_000;
46
47/// Default ordinary user allocation in raw native token units (100 native tokens).
48pub const DEFAULT_LOCAL_ACCOUNT_TOKENS: u64 = 1_000_000_000;
49
50/// Bootstrap manifest kind consumed by hog-devnode.
51pub const HOGD_DEV_BOOTSTRAP_MANIFEST_KIND: &str = "groundhog-bootstrap";
52
53/// Default bootstrap profile requested from hogd-dev.
54pub const DEFAULT_HOGD_DEV_BOOTSTRAP_PROFILE: &str = "local-dev";
55
56/// Default storage-cost model profile requested from hogd-dev.
57pub const DEFAULT_HOGD_DEV_STORAGE_COST_MODEL_PROFILE: &str = "hogd-default";
58
59/// Default local airdrop amount: 10 native tokens in indivisible units.
60pub const DEFAULT_LOCAL_AIRDROP_RAW_AMOUNT: u128 = 10 * thruput_signing::NATIVE_TOKEN_SCALE;
61
62/// Resource bounds and transfer amount for the optional local airdrop RPC.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct LocalAirdropConfig {
66    /// Native amount transferred by each logical attempt.
67    pub raw_amount: u128,
68    /// Maximum retained requests for the chain lifetime; completed requests remain retained.
69    pub attempt_capacity: usize,
70    /// Maximum concurrently advancing attempts.
71    pub max_concurrency: usize,
72    /// End-to-end backend deadline for one attempt advancement.
73    pub request_timeout: Duration,
74}
75
76impl Default for LocalAirdropConfig {
77    fn default() -> Self {
78        Self {
79            raw_amount: DEFAULT_LOCAL_AIRDROP_RAW_AMOUNT,
80            attempt_capacity: 1_024,
81            max_concurrency: 16,
82            request_timeout: Duration::from_secs(30),
83        }
84    }
85}
86
87impl LocalAirdropConfig {
88    pub(crate) fn validate(self) -> Result<Self, &'static str> {
89        if self.raw_amount == 0 {
90            return Err("raw amount must be greater than zero");
91        }
92        if self.attempt_capacity == 0 {
93            return Err("attempt capacity must be greater than zero");
94        }
95        if self.max_concurrency == 0 {
96            return Err("maximum concurrency must be greater than zero");
97        }
98        if self.request_timeout.is_zero() {
99            return Err("request timeout must be greater than zero");
100        }
101        Ok(self)
102    }
103}
104
105/// Supervisor configuration for a local Thruput node.
106#[derive(Debug, Clone, PartialEq, Eq)]
107#[non_exhaustive]
108pub struct NodeSupervisorConfig {
109    /// Public JSON-RPC listen address exposed by the Rust facade.
110    pub rpc_listen_address: String,
111    /// Maximum simultaneous public JSON-RPC connections.
112    pub rpc_max_connections: u32,
113    /// Active plus buffered entries per JSON-RPC array (1..=1024, default 128).
114    pub rpc_batch_concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency,
115    /// Configuration for the supervised hogd-dev child process.
116    pub hogd_dev: HogdDevConfig,
117    /// Optional protocol string used for hogd-dev intro handshakes.
118    ///
119    /// Defaults to the protocol embedded in the linked `thruput-xdr` crate.
120    /// This override is intended for local cross-repo smoke tests before a
121    /// matching Rust protocol crate has been published.
122    pub hogd_protocol_override: Option<String>,
123    /// Gas budget assigned to hogd-dev `ViewGateway` root invocations.
124    pub view_gas_limit: u64,
125    /// Maximum time to wait for one hogd-dev `ViewGateway` execution RPC.
126    pub view_timeout: Duration,
127    /// Aggregate limits for shared-snapshot reads.
128    pub view_batch: thruput_json_rpc_server::ViewBatchConfig,
129    /// Local block production policy for hogd-dev scheduler coordination.
130    pub block_driver: BlockDriverConfig,
131    /// Optional local airdrop behavior and resource bounds.
132    pub local_airdrop: LocalAirdropConfig,
133    /// Validated release identity supplied by the binary embedding the supervisor.
134    pub bundle_identity: Option<BundleIdentity>,
135}
136
137impl Default for NodeSupervisorConfig {
138    fn default() -> Self {
139        Self {
140            rpc_listen_address: DEFAULT_RPC_LISTEN_ADDRESS.to_string(),
141            rpc_max_connections: DEFAULT_RPC_MAX_CONNECTIONS,
142            rpc_batch_concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency::default(),
143            hogd_dev: HogdDevConfig::default(),
144            hogd_protocol_override: None,
145            view_gas_limit: thruput_json_rpc_server::DEFAULT_VIEW_GAS_LIMIT,
146            view_timeout: DEFAULT_HOGD_DEV_VIEW_TIMEOUT,
147            view_batch: thruput_json_rpc_server::ViewBatchConfig::default(),
148            block_driver: BlockDriverConfig::default(),
149            local_airdrop: LocalAirdropConfig::default(),
150            bundle_identity: None,
151        }
152    }
153}
154
155impl NodeSupervisorConfig {
156    /// Set aggregate shared-read limits. Per-call view limits also apply.
157    #[must_use]
158    pub const fn with_view_batch(
159        mut self,
160        limits: thruput_json_rpc_server::ViewBatchConfig,
161    ) -> Self {
162        self.view_batch = limits;
163        self
164    }
165    /// Return a copy with a validated per-array JSON-RPC concurrency limit.
166    #[must_use]
167    pub const fn with_rpc_batch_concurrency(
168        mut self,
169        concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency,
170    ) -> Self {
171        self.rpc_batch_concurrency = concurrency;
172        self
173    }
174    /// Return a copy with a different public JSON-RPC listen address.
175    #[must_use]
176    pub fn with_rpc_listen_address(mut self, listen_address: impl Into<String>) -> Self {
177        self.rpc_listen_address = listen_address.into();
178        self
179    }
180
181    /// Return a copy with a different public JSON-RPC connection cap.
182    #[must_use]
183    pub const fn with_rpc_max_connections(mut self, max_connections: u32) -> Self {
184        self.rpc_max_connections = max_connections;
185        self
186    }
187
188    /// Return a copy with an explicit hogd-dev binary path.
189    #[must_use]
190    pub fn with_hogd_dev_binary(mut self, binary_path: impl Into<PathBuf>) -> Self {
191        self.hogd_dev.binary_path = Some(binary_path.into());
192        self
193    }
194
195    /// Return a copy with the supplied hogd-dev configuration.
196    #[must_use]
197    pub fn with_hogd_dev(mut self, hogd_dev: HogdDevConfig) -> Self {
198        self.hogd_dev = hogd_dev;
199        self
200    }
201
202    /// Return a copy with an explicit hogd protocol string for intro probes.
203    ///
204    /// This is a development escape hatch. It should only be used when the
205    /// caller has verified that the linked Rust protocol structs are compatible
206    /// with the hogd-dev binary being tested.
207    #[must_use]
208    pub fn with_hogd_protocol_override(mut self, protocol: impl Into<String>) -> Self {
209        self.hogd_protocol_override = Some(protocol.into());
210        self
211    }
212
213    /// Return a copy with a different hogd-dev `ViewGateway` gas budget.
214    #[must_use]
215    pub fn with_view_gas_limit(mut self, view_gas_limit: u64) -> Self {
216        self.view_gas_limit = view_gas_limit;
217        self
218    }
219
220    /// Return a copy with a different hogd-dev `ViewGateway` RPC timeout.
221    #[must_use]
222    pub fn with_view_timeout(mut self, view_timeout: Duration) -> Self {
223        self.view_timeout = view_timeout;
224        self
225    }
226
227    /// Return a copy with a different local block production policy.
228    #[must_use]
229    pub fn with_block_driver(mut self, block_driver: BlockDriverConfig) -> Self {
230        self.block_driver = block_driver;
231        self
232    }
233
234    /// Return a copy with different local airdrop controls.
235    #[must_use]
236    pub const fn with_local_airdrop(mut self, local_airdrop: LocalAirdropConfig) -> Self {
237        self.local_airdrop = local_airdrop;
238        self
239    }
240
241    /// Return a copy bound to the embedding binary's validated release identity.
242    #[must_use]
243    pub fn with_bundle_identity(mut self, bundle_identity: BundleIdentity) -> Self {
244        self.bundle_identity = Some(bundle_identity);
245        self
246    }
247}
248
249/// Local block production policy used by the hogd-dev supervisor.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251#[non_exhaustive]
252pub struct BlockDriverConfig {
253    /// Driver wake mode.
254    pub mode: BlockDriverMode,
255    /// Timer interval for [`BlockDriverMode::Interval`].
256    pub interval: Duration,
257    /// Execution deadline once a request reaches the manual driver.
258    ///
259    /// Time spent waiting behind an earlier request is not included.
260    pub manual_request_timeout: Duration,
261}
262
263impl BlockDriverConfig {
264    /// Return an instant submit-wake policy.
265    #[must_use]
266    pub const fn instant() -> Self {
267        Self {
268            mode: BlockDriverMode::Instant,
269            interval: DEFAULT_BLOCK_DRIVER_INTERVAL,
270            manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
271        }
272    }
273
274    /// Return a fixed-interval policy.
275    #[must_use]
276    pub const fn interval(interval: Duration) -> Self {
277        Self {
278            mode: BlockDriverMode::Interval,
279            interval,
280            manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
281        }
282    }
283
284    /// Return an explicitly-triggered block production policy.
285    #[must_use]
286    pub const fn manual() -> Self {
287        Self {
288            mode: BlockDriverMode::Manual,
289            interval: DEFAULT_BLOCK_DRIVER_INTERVAL,
290            manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
291        }
292    }
293
294    /// Return a copy with a different mode.
295    #[must_use]
296    pub const fn with_mode(mut self, mode: BlockDriverMode) -> Self {
297        self.mode = mode;
298        self
299    }
300
301    /// Return a copy with a different interval.
302    #[must_use]
303    pub const fn with_interval(mut self, interval: Duration) -> Self {
304        self.interval = interval;
305        self
306    }
307
308    /// Return a copy with a different manual request deadline.
309    #[must_use]
310    pub const fn with_manual_request_timeout(mut self, timeout: Duration) -> Self {
311        self.manual_request_timeout = timeout;
312        self
313    }
314
315    pub(crate) fn effective_interval(self) -> Duration {
316        if self.interval.is_zero() {
317            DEFAULT_BLOCK_DRIVER_INTERVAL
318        } else {
319            self.interval
320        }
321    }
322
323    pub(crate) fn effective_manual_request_timeout(self) -> Duration {
324        if self.manual_request_timeout.is_zero() {
325            DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT
326        } else {
327            self.manual_request_timeout
328        }
329    }
330}
331
332impl Default for BlockDriverConfig {
333    fn default() -> Self {
334        Self::instant()
335    }
336}
337
338/// Block driver wake mode.
339#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
340#[non_exhaustive]
341pub enum BlockDriverMode {
342    /// Submit-plane successes wake the block driver immediately.
343    #[default]
344    Instant,
345    /// A fixed timer drives nomination/commit attempts.
346    Interval,
347    /// Blocks are produced only through the supervisor's in-process control handle.
348    Manual,
349}
350
351/// Log verbosity level for hog-devnode
352#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
353pub enum LogLevel {
354    Critical,
355    Error,
356    #[default]
357    Warn,
358    Info,
359    Debug,
360    Trace,
361}
362
363impl LogLevel {
364    pub fn as_str(&self) -> &str {
365        match self {
366            LogLevel::Critical => "critical",
367            LogLevel::Error => "error",
368            LogLevel::Warn => "warn",
369            LogLevel::Info => "info",
370            LogLevel::Debug => "debug",
371            LogLevel::Trace => "trace",
372        }
373    }
374}
375
376/// Configuration for the supervised hogd-dev process.
377#[derive(Debug, Clone, PartialEq, Eq)]
378#[non_exhaustive]
379pub struct HogdDevConfig {
380    /// Explicit binary path, if supplied by CLI flag or higher-level config.
381    ///
382    /// When unset, the supervisor falls back to the documented hogd-dev binary
383    /// lookup order.
384    pub binary_path: Option<PathBuf>,
385    /// Contract artifact directories searched by hog-devnode before its defaults.
386    pub contract_artifact_directories: Vec<PathBuf>,
387    /// Directory where hog-devnode persists recoverable node state.
388    ///
389    /// When unset, hog-devnode runs with ephemeral state.
390    pub data_dir: Option<PathBuf>,
391    /// Log level to request from hogd-dev
392    pub log_level: Option<LogLevel>,
393    /// Host address hogd-dev should bind its internal services to.
394    pub listen_host: String,
395    /// Grace period after SIGTERM before the supervisor escalates to SIGKILL.
396    ///
397    /// This allows log drain and orderly task cancellation before forced
398    /// process termination.
399    pub shutdown_grace: Duration,
400    /// Maximum time to wait for the readiness line after spawning hogd-dev.
401    pub startup_timeout: Duration,
402    /// Explicit public allocations for a fresh local chain.
403    ///
404    /// On restart the persisted public intent remains authoritative.
405    pub bootstrap_accounts: Vec<BootstrapManifestAllocation>,
406    /// RAM database capacity in bytes hog-devnode reserves at startup.
407    ///
408    /// When unset, hog-devnode uses its built-in default capacity.
409    pub ramdb_bytes: Option<u64>,
410    /// Genesis execution parameter values forwarded to hog-devnode in place
411    /// of the ones stock genesis states. The child still rejects a value
412    /// outside the ceilings its build was compiled with.
413    pub network_config_overrides: BTreeMap<ConfigParamName, u64>,
414    /// Raw arguments appended after every flag the supervisor sets, so a
415    /// repeated flag here overrides the supervisor's value. Not persisted.
416    pub extra_args: Vec<OsString>,
417    /// Signing domain for a new chain. Omission initializes the zero development domain.
418    ///
419    /// An initialized directory rejects this option, even if the value matches.
420    /// Leave it unset on restart to load the persisted identity.
421    pub init_network_hash: Option<NetworkHashText>,
422    /// PID file used to detect and clean up orphaned hogd-dev children.
423    pub pid_file: Option<PathBuf>,
424    /// Environment for the child only, so tests can steer the fake hogd-dev
425    /// without changing the test process's environment.
426    #[cfg(test)]
427    pub(crate) test_child_env: Vec<(OsString, OsString)>,
428}
429
430impl Default for HogdDevConfig {
431    fn default() -> Self {
432        Self {
433            binary_path: None,
434            contract_artifact_directories: Vec::new(),
435            data_dir: None,
436            log_level: None,
437            listen_host: DEFAULT_HOGD_DEV_LISTEN_HOST.to_string(),
438            shutdown_grace: DEFAULT_HOGD_DEV_SHUTDOWN_GRACE,
439            startup_timeout: DEFAULT_HOGD_DEV_STARTUP_TIMEOUT,
440            bootstrap_accounts: Vec::new(),
441            ramdb_bytes: None,
442            network_config_overrides: BTreeMap::new(),
443            extra_args: Vec::new(),
444            init_network_hash: None,
445            pid_file: None,
446            #[cfg(test)]
447            test_child_env: Vec::new(),
448        }
449    }
450}
451
452impl HogdDevConfig {
453    /// Return a copy that sets `key` in the child's environment.
454    #[cfg(test)]
455    #[must_use]
456    pub(crate) fn with_test_child_env(
457        mut self,
458        key: impl Into<OsString>,
459        value: impl Into<OsString>,
460    ) -> Self {
461        self.test_child_env.push((key.into(), value.into()));
462        self
463    }
464
465    /// Select a new chain's signing domain. Initialized directories reject this option.
466    #[must_use]
467    pub fn with_init_network_hash(mut self, network_hash: NetworkHashText) -> Self {
468        self.init_network_hash = Some(network_hash);
469        self
470    }
471
472    /// Return a copy with genesis network parameter overrides.
473    #[must_use]
474    pub fn with_network_config_overrides(
475        mut self,
476        overrides: BTreeMap<ConfigParamName, u64>,
477    ) -> Self {
478        self.network_config_overrides = overrides;
479        self
480    }
481
482    /// Return a copy with an explicit hogd-dev binary path.
483    #[must_use]
484    pub fn with_binary_path(mut self, binary_path: impl Into<PathBuf>) -> Self {
485        self.binary_path = Some(binary_path.into());
486        self
487    }
488
489    /// Return a copy with explicit contract artifact search directories.
490    #[must_use]
491    pub fn with_contract_artifact_directories<I, P>(mut self, directories: I) -> Self
492    where
493        I: IntoIterator<Item = P>,
494        P: Into<PathBuf>,
495    {
496        self.contract_artifact_directories = directories
497            .into_iter()
498            .map(Into::into)
499            .map(resolve_path)
500            .collect();
501        self
502    }
503
504    /// Return a copy with a directory for persistent hog-devnode state.
505    #[must_use]
506    pub fn with_data_dir(mut self, data_dir: impl Into<PathBuf>) -> Self {
507        self.data_dir = Some(resolve_path(data_dir.into()));
508        self
509    }
510
511    /// Return a copy with an explicit hogd-dev log level.
512    #[must_use]
513    pub fn with_log_level(mut self, log_level: LogLevel) -> Self {
514        self.log_level = Some(log_level);
515        self
516    }
517
518    /// Return a copy with a different internal listen host.
519    #[must_use]
520    pub fn with_listen_host(mut self, listen_host: impl Into<String>) -> Self {
521        self.listen_host = listen_host.into();
522        self
523    }
524
525    /// Return a copy with a different graceful shutdown timeout.
526    #[must_use]
527    pub fn with_shutdown_grace(mut self, shutdown_grace: Duration) -> Self {
528        self.shutdown_grace = shutdown_grace;
529        self
530    }
531
532    /// Return a copy with a different startup readiness timeout.
533    #[must_use]
534    pub fn with_startup_timeout(mut self, startup_timeout: Duration) -> Self {
535        self.startup_timeout = startup_timeout;
536        self
537    }
538
539    /// Return a copy with caller-owned public bootstrap allocations.
540    #[must_use]
541    pub fn with_bootstrap_accounts(mut self, accounts: Vec<BootstrapManifestAllocation>) -> Self {
542        self.bootstrap_accounts = accounts;
543        self
544    }
545
546    /// Return a copy with an explicit hog-devnode RAM database capacity in bytes.
547    #[must_use]
548    pub fn with_ramdb_bytes(mut self, ramdb_bytes: u64) -> Self {
549        self.ramdb_bytes = Some(ramdb_bytes);
550        self
551    }
552
553    /// Return a copy with raw arguments appended to the hog-devnode command line.
554    #[must_use]
555    pub fn with_extra_args<I, A>(mut self, args: I) -> Self
556    where
557        I: IntoIterator<Item = A>,
558        A: Into<OsString>,
559    {
560        self.extra_args = args.into_iter().map(Into::into).collect();
561        self
562    }
563
564    /// Return a copy with a PID file for orphan detection.
565    #[must_use]
566    pub fn with_pid_file(mut self, pid_file: impl Into<PathBuf>) -> Self {
567        self.pid_file = Some(pid_file.into());
568        self
569    }
570
571    /// Build public intent using the supervisor-owned faucet's public key.
572    pub fn to_bootstrap_manifest(
573        &self,
574        network_hash: NetworkHashText,
575        faucet_public_key: Ed25519PublicKey,
576    ) -> BootstrapManifest {
577        BootstrapManifest::new(
578            HOGD_DEV_BOOTSTRAP_MANIFEST_KIND,
579            network_hash,
580            DEFAULT_HOGD_DEV_BOOTSTRAP_PROFILE,
581            BootstrapManifestAllocation::new(
582                faucet_public_key,
583                LOCAL_FAUCET_STORAGE_TOKENS.to_string(),
584            ),
585            self.bootstrap_accounts.clone(),
586            BootstrapManifestStorageCostModel::profile(DEFAULT_HOGD_DEV_STORAGE_COST_MODEL_PROFILE),
587        )
588    }
589}
590
591fn resolve_path(path: PathBuf) -> PathBuf {
592    let expanded = match path.strip_prefix(Path::new("~")) {
593        Ok(rest) => std::env::var_os("HOME")
594            .map(PathBuf::from)
595            .map(|home| home.join(rest))
596            .unwrap_or(path),
597        Err(_) => path,
598    };
599    std::path::absolute(&expanded).unwrap_or(expanded)
600}
601
602#[cfg(test)]
603mod tests {
604    use super::LocalAirdropConfig;
605
606    #[test]
607    fn local_airdrop_default_is_valid() {
608        assert!(LocalAirdropConfig::default().validate().is_ok());
609    }
610
611    #[test]
612    fn local_airdrop_rejects_zero_resource_bounds() {
613        let default = LocalAirdropConfig::default();
614        let invalid = [
615            LocalAirdropConfig {
616                raw_amount: 0,
617                ..default
618            },
619            LocalAirdropConfig {
620                attempt_capacity: 0,
621                ..default
622            },
623            LocalAirdropConfig {
624                max_concurrency: 0,
625                ..default
626            },
627            LocalAirdropConfig {
628                request_timeout: std::time::Duration::ZERO,
629                ..default
630            },
631        ];
632
633        assert!(invalid.into_iter().all(|config| config.validate().is_err()));
634    }
635}