use std::{
collections::BTreeMap,
ffi::OsString,
path::{Path, PathBuf},
time::Duration,
};
use thruput_bundle_identity::BundleIdentity;
use thruput_json_rpc_types::bootstrap_manifest::{
BootstrapManifest, BootstrapManifestAllocation, BootstrapManifestStorageCostModel,
};
use thruput_json_rpc_types::{Ed25519PublicKey, NetworkHashText};
use thruput_tx_validation::ConfigParamName;
pub const DEFAULT_RPC_LISTEN_ADDRESS: &str = "127.0.0.1:9545";
pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 2_000;
pub const DEFAULT_HOGD_DEV_VIEW_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_BLOCK_DRIVER_INTERVAL: Duration = Duration::from_millis(500);
pub const DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_HOGD_DEV_LISTEN_HOST: &str = "127.0.0.1";
pub const DEFAULT_HOGD_DEV_SHUTDOWN_GRACE: Duration = Duration::from_secs(5);
pub const DEFAULT_HOGD_DEV_PERSISTENT_SHUTDOWN_GRACE: Duration = Duration::from_secs(30);
pub const DEFAULT_HOGD_DEV_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
pub const LOCAL_FAUCET_STORAGE_TOKENS: u64 = 10_000_000_000_000;
pub const DEFAULT_LOCAL_ACCOUNT_TOKENS: u64 = 1_000_000_000;
pub const HOGD_DEV_BOOTSTRAP_MANIFEST_KIND: &str = "groundhog-bootstrap";
pub const DEFAULT_HOGD_DEV_BOOTSTRAP_PROFILE: &str = "local-dev";
pub const DEFAULT_HOGD_DEV_STORAGE_COST_MODEL_PROFILE: &str = "hogd-default";
pub const DEFAULT_LOCAL_AIRDROP_RAW_AMOUNT: u128 = 10 * thruput_signing::NATIVE_TOKEN_SCALE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LocalAirdropConfig {
pub raw_amount: u128,
pub attempt_capacity: usize,
pub max_concurrency: usize,
pub request_timeout: Duration,
}
impl Default for LocalAirdropConfig {
fn default() -> Self {
Self {
raw_amount: DEFAULT_LOCAL_AIRDROP_RAW_AMOUNT,
attempt_capacity: 1_024,
max_concurrency: 16,
request_timeout: Duration::from_secs(30),
}
}
}
impl LocalAirdropConfig {
pub(crate) fn validate(self) -> Result<Self, &'static str> {
if self.raw_amount == 0 {
return Err("raw amount must be greater than zero");
}
if self.attempt_capacity == 0 {
return Err("attempt capacity must be greater than zero");
}
if self.max_concurrency == 0 {
return Err("maximum concurrency must be greater than zero");
}
if self.request_timeout.is_zero() {
return Err("request timeout must be greater than zero");
}
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NodeSupervisorConfig {
pub rpc_listen_address: String,
pub rpc_max_connections: u32,
pub rpc_batch_concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency,
pub hogd_dev: HogdDevConfig,
pub hogd_protocol_override: Option<String>,
pub view_gas_limit: u64,
pub view_timeout: Duration,
pub view_batch: thruput_json_rpc_server::ViewBatchConfig,
pub block_driver: BlockDriverConfig,
pub local_airdrop: LocalAirdropConfig,
pub bundle_identity: Option<BundleIdentity>,
}
impl Default for NodeSupervisorConfig {
fn default() -> Self {
Self {
rpc_listen_address: DEFAULT_RPC_LISTEN_ADDRESS.to_string(),
rpc_max_connections: DEFAULT_RPC_MAX_CONNECTIONS,
rpc_batch_concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency::default(),
hogd_dev: HogdDevConfig::default(),
hogd_protocol_override: None,
view_gas_limit: thruput_json_rpc_server::DEFAULT_VIEW_GAS_LIMIT,
view_timeout: DEFAULT_HOGD_DEV_VIEW_TIMEOUT,
view_batch: thruput_json_rpc_server::ViewBatchConfig::default(),
block_driver: BlockDriverConfig::default(),
local_airdrop: LocalAirdropConfig::default(),
bundle_identity: None,
}
}
}
impl NodeSupervisorConfig {
#[must_use]
pub const fn with_view_batch(
mut self,
limits: thruput_json_rpc_server::ViewBatchConfig,
) -> Self {
self.view_batch = limits;
self
}
#[must_use]
pub const fn with_rpc_batch_concurrency(
mut self,
concurrency: thruput_json_rpc_server::batch::RpcBatchConcurrency,
) -> Self {
self.rpc_batch_concurrency = concurrency;
self
}
#[must_use]
pub fn with_rpc_listen_address(mut self, listen_address: impl Into<String>) -> Self {
self.rpc_listen_address = listen_address.into();
self
}
#[must_use]
pub const fn with_rpc_max_connections(mut self, max_connections: u32) -> Self {
self.rpc_max_connections = max_connections;
self
}
#[must_use]
pub fn with_hogd_dev_binary(mut self, binary_path: impl Into<PathBuf>) -> Self {
self.hogd_dev.binary_path = Some(binary_path.into());
self
}
#[must_use]
pub fn with_hogd_dev(mut self, hogd_dev: HogdDevConfig) -> Self {
self.hogd_dev = hogd_dev;
self
}
#[must_use]
pub fn with_hogd_protocol_override(mut self, protocol: impl Into<String>) -> Self {
self.hogd_protocol_override = Some(protocol.into());
self
}
#[must_use]
pub fn with_view_gas_limit(mut self, view_gas_limit: u64) -> Self {
self.view_gas_limit = view_gas_limit;
self
}
#[must_use]
pub fn with_view_timeout(mut self, view_timeout: Duration) -> Self {
self.view_timeout = view_timeout;
self
}
#[must_use]
pub fn with_block_driver(mut self, block_driver: BlockDriverConfig) -> Self {
self.block_driver = block_driver;
self
}
#[must_use]
pub const fn with_local_airdrop(mut self, local_airdrop: LocalAirdropConfig) -> Self {
self.local_airdrop = local_airdrop;
self
}
#[must_use]
pub fn with_bundle_identity(mut self, bundle_identity: BundleIdentity) -> Self {
self.bundle_identity = Some(bundle_identity);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BlockDriverConfig {
pub mode: BlockDriverMode,
pub interval: Duration,
pub manual_request_timeout: Duration,
}
impl BlockDriverConfig {
#[must_use]
pub const fn instant() -> Self {
Self {
mode: BlockDriverMode::Instant,
interval: DEFAULT_BLOCK_DRIVER_INTERVAL,
manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
}
}
#[must_use]
pub const fn interval(interval: Duration) -> Self {
Self {
mode: BlockDriverMode::Interval,
interval,
manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
}
}
#[must_use]
pub const fn manual() -> Self {
Self {
mode: BlockDriverMode::Manual,
interval: DEFAULT_BLOCK_DRIVER_INTERVAL,
manual_request_timeout: DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT,
}
}
#[must_use]
pub const fn with_mode(mut self, mode: BlockDriverMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub const fn with_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[must_use]
pub const fn with_manual_request_timeout(mut self, timeout: Duration) -> Self {
self.manual_request_timeout = timeout;
self
}
pub(crate) fn effective_interval(self) -> Duration {
if self.interval.is_zero() {
DEFAULT_BLOCK_DRIVER_INTERVAL
} else {
self.interval
}
}
pub(crate) fn effective_manual_request_timeout(self) -> Duration {
if self.manual_request_timeout.is_zero() {
DEFAULT_MANUAL_BLOCK_REQUEST_TIMEOUT
} else {
self.manual_request_timeout
}
}
}
impl Default for BlockDriverConfig {
fn default() -> Self {
Self::instant()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum BlockDriverMode {
#[default]
Instant,
Interval,
Manual,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LogLevel {
Critical,
Error,
#[default]
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
pub fn as_str(&self) -> &str {
match self {
LogLevel::Critical => "critical",
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HogdDevConfig {
pub binary_path: Option<PathBuf>,
pub contract_artifact_directories: Vec<PathBuf>,
pub data_dir: Option<PathBuf>,
pub log_level: Option<LogLevel>,
pub listen_host: String,
pub shutdown_grace: Duration,
pub startup_timeout: Duration,
pub bootstrap_accounts: Vec<BootstrapManifestAllocation>,
pub ramdb_bytes: Option<u64>,
pub network_config_overrides: BTreeMap<ConfigParamName, u64>,
pub extra_args: Vec<OsString>,
pub init_network_hash: Option<NetworkHashText>,
pub pid_file: Option<PathBuf>,
#[cfg(test)]
pub(crate) test_child_env: Vec<(OsString, OsString)>,
}
impl Default for HogdDevConfig {
fn default() -> Self {
Self {
binary_path: None,
contract_artifact_directories: Vec::new(),
data_dir: None,
log_level: None,
listen_host: DEFAULT_HOGD_DEV_LISTEN_HOST.to_string(),
shutdown_grace: DEFAULT_HOGD_DEV_SHUTDOWN_GRACE,
startup_timeout: DEFAULT_HOGD_DEV_STARTUP_TIMEOUT,
bootstrap_accounts: Vec::new(),
ramdb_bytes: None,
network_config_overrides: BTreeMap::new(),
extra_args: Vec::new(),
init_network_hash: None,
pid_file: None,
#[cfg(test)]
test_child_env: Vec::new(),
}
}
}
impl HogdDevConfig {
#[cfg(test)]
#[must_use]
pub(crate) fn with_test_child_env(
mut self,
key: impl Into<OsString>,
value: impl Into<OsString>,
) -> Self {
self.test_child_env.push((key.into(), value.into()));
self
}
#[must_use]
pub fn with_init_network_hash(mut self, network_hash: NetworkHashText) -> Self {
self.init_network_hash = Some(network_hash);
self
}
#[must_use]
pub fn with_network_config_overrides(
mut self,
overrides: BTreeMap<ConfigParamName, u64>,
) -> Self {
self.network_config_overrides = overrides;
self
}
#[must_use]
pub fn with_binary_path(mut self, binary_path: impl Into<PathBuf>) -> Self {
self.binary_path = Some(binary_path.into());
self
}
#[must_use]
pub fn with_contract_artifact_directories<I, P>(mut self, directories: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
self.contract_artifact_directories = directories
.into_iter()
.map(Into::into)
.map(resolve_path)
.collect();
self
}
#[must_use]
pub fn with_data_dir(mut self, data_dir: impl Into<PathBuf>) -> Self {
self.data_dir = Some(resolve_path(data_dir.into()));
self
}
#[must_use]
pub fn with_log_level(mut self, log_level: LogLevel) -> Self {
self.log_level = Some(log_level);
self
}
#[must_use]
pub fn with_listen_host(mut self, listen_host: impl Into<String>) -> Self {
self.listen_host = listen_host.into();
self
}
#[must_use]
pub fn with_shutdown_grace(mut self, shutdown_grace: Duration) -> Self {
self.shutdown_grace = shutdown_grace;
self
}
#[must_use]
pub fn with_startup_timeout(mut self, startup_timeout: Duration) -> Self {
self.startup_timeout = startup_timeout;
self
}
#[must_use]
pub fn with_bootstrap_accounts(mut self, accounts: Vec<BootstrapManifestAllocation>) -> Self {
self.bootstrap_accounts = accounts;
self
}
#[must_use]
pub fn with_ramdb_bytes(mut self, ramdb_bytes: u64) -> Self {
self.ramdb_bytes = Some(ramdb_bytes);
self
}
#[must_use]
pub fn with_extra_args<I, A>(mut self, args: I) -> Self
where
I: IntoIterator<Item = A>,
A: Into<OsString>,
{
self.extra_args = args.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_pid_file(mut self, pid_file: impl Into<PathBuf>) -> Self {
self.pid_file = Some(pid_file.into());
self
}
pub fn to_bootstrap_manifest(
&self,
network_hash: NetworkHashText,
faucet_public_key: Ed25519PublicKey,
) -> BootstrapManifest {
BootstrapManifest::new(
HOGD_DEV_BOOTSTRAP_MANIFEST_KIND,
network_hash,
DEFAULT_HOGD_DEV_BOOTSTRAP_PROFILE,
BootstrapManifestAllocation::new(
faucet_public_key,
LOCAL_FAUCET_STORAGE_TOKENS.to_string(),
),
self.bootstrap_accounts.clone(),
BootstrapManifestStorageCostModel::profile(DEFAULT_HOGD_DEV_STORAGE_COST_MODEL_PROFILE),
)
}
}
fn resolve_path(path: PathBuf) -> PathBuf {
let expanded = match path.strip_prefix(Path::new("~")) {
Ok(rest) => std::env::var_os("HOME")
.map(PathBuf::from)
.map(|home| home.join(rest))
.unwrap_or(path),
Err(_) => path,
};
std::path::absolute(&expanded).unwrap_or(expanded)
}
#[cfg(test)]
mod tests {
use super::LocalAirdropConfig;
#[test]
fn local_airdrop_default_is_valid() {
assert!(LocalAirdropConfig::default().validate().is_ok());
}
#[test]
fn local_airdrop_rejects_zero_resource_bounds() {
let default = LocalAirdropConfig::default();
let invalid = [
LocalAirdropConfig {
raw_amount: 0,
..default
},
LocalAirdropConfig {
attempt_capacity: 0,
..default
},
LocalAirdropConfig {
max_concurrency: 0,
..default
},
LocalAirdropConfig {
request_timeout: std::time::Duration::ZERO,
..default
},
];
assert!(invalid.into_iter().all(|config| config.validate().is_err()));
}
}