use std::path::{Path, PathBuf};
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Serialize};
use crate::errors::{CommonError, Result};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeConfig {
#[serde(default)]
pub node: NodeSection,
#[serde(default)]
pub roles: RolesSection,
#[serde(default)]
pub resources: ResourcesSection,
#[serde(default)]
pub operator: OperatorSection,
#[serde(default)]
pub network: NetworkSection,
#[serde(default)]
pub telemetry: TelemetrySection,
#[serde(default)]
pub tee: TeeSection,
#[serde(default)]
pub da: DaSection,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeSection {
pub name: String,
pub network: String,
pub data_dir: PathBuf,
pub log_level: String,
}
impl Default for NodeSection {
fn default() -> Self {
Self {
name: "arknet-node".into(),
network: "devnet".into(),
data_dir: PathBuf::from("/var/lib/arknet"),
log_level: "info".into(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RolesSection {
#[serde(default)]
pub validator: bool,
#[serde(default)]
pub router: bool,
#[serde(default)]
pub compute: bool,
#[serde(default)]
pub verifier: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ResourcesSection {
#[serde(default)]
pub compute: ComputeResources,
#[serde(default)]
pub router: RouterResources,
#[serde(default)]
pub verifier: VerifierResources,
#[serde(default)]
pub validator: ValidatorResources,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ComputeResources {
#[serde(default)]
pub gpu_devices: Vec<u32>,
#[serde(default)]
pub max_vram_gb: u32,
#[serde(default)]
pub max_concurrent_jobs: u32,
#[serde(default)]
pub loaded_models: Vec<String>,
#[serde(default)]
pub model_swap_enabled: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RouterResources {
#[serde(default)]
pub cpu_percent: u32,
#[serde(default)]
pub max_concurrent_routes: u32,
#[serde(default)]
pub bandwidth_mbps: u32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerifierResources {
#[serde(default)]
pub gpu_share_with_compute: bool,
#[serde(default)]
pub max_verifications_per_hour: u32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ValidatorResources {
#[serde(default)]
pub state_db_path: Option<PathBuf>,
#[serde(default)]
pub gossip_peers_max: u32,
#[serde(default)]
pub remote_signer: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperatorSection {
#[serde(default)]
pub payout_address: Option<String>,
#[serde(default)]
pub preferred_region: Option<String>,
#[serde(default)]
pub min_reward_per_job: Option<String>,
#[serde(default)]
pub auto_model_pull: bool,
#[serde(default)]
pub auto_pool_rebalance: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkSection {
pub p2p_listen: String,
pub rpc_listen: String,
pub metrics_listen: String,
#[serde(default)]
pub bootstrap_peers: Vec<String>,
#[serde(default)]
pub external_address: Option<String>,
#[serde(default)]
pub max_inbound_peers: u32,
#[serde(default)]
pub max_outbound_peers: u32,
}
impl Default for NetworkSection {
fn default() -> Self {
Self {
p2p_listen: "0.0.0.0:26656".into(),
rpc_listen: "127.0.0.1:26657".into(),
metrics_listen: "127.0.0.1:9090".into(),
bootstrap_peers: Vec::new(),
external_address: None,
max_inbound_peers: 60,
max_outbound_peers: 20,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TelemetrySection {
#[serde(default)]
pub prometheus_enabled: bool,
#[serde(default)]
pub otlp_endpoint: Option<String>,
#[serde(default)]
pub sentry_dsn: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TeeSection {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub enclave_key_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DaSection {
#[serde(default)]
pub layer: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub namespace: String,
#[serde(default)]
pub auth_token: String,
}
impl NodeConfig {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
Figment::new()
.merge(Serialized::defaults(NodeConfig::default()))
.merge(Toml::file(path.as_ref()))
.merge(Env::prefixed("ARKNET_").split("__"))
.extract::<Self>()
.map_err(|e| CommonError::Config(e.to_string()))
}
pub fn load_env_only() -> Result<Self> {
Figment::new()
.merge(Serialized::defaults(NodeConfig::default()))
.merge(Env::prefixed("ARKNET_").split("__"))
.extract::<Self>()
.map_err(|e| CommonError::Config(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sensible() {
let c = NodeConfig::default();
assert_eq!(c.node.network, "devnet");
assert_eq!(c.node.log_level, "info");
assert!(!c.roles.validator);
assert!(!c.roles.router);
assert!(!c.roles.compute);
assert!(!c.roles.verifier);
assert_eq!(c.network.p2p_listen, "0.0.0.0:26656");
}
#[test]
fn loads_minimal_toml() {
let tmp = tempdir();
let path = tmp.path().join("node.toml");
std::fs::write(
&path,
r#"
[node]
name = "test-node"
network = "testnet"
data_dir = "/tmp/arknet-test"
log_level = "debug"
[roles]
router = true
compute = true
"#,
)
.unwrap();
let c = NodeConfig::load(&path).expect("config loads");
assert_eq!(c.node.name, "test-node");
assert_eq!(c.node.network, "testnet");
assert!(c.roles.router);
assert!(c.roles.compute);
assert!(!c.roles.validator);
}
#[test]
fn rejects_unknown_fields() {
let tmp = tempdir();
let path = tmp.path().join("node.toml");
std::fs::write(
&path,
r#"
[node]
name = "x"
network = "devnet"
data_dir = "/tmp"
log_level = "info"
mystery_field = 42
"#,
)
.unwrap();
let res = NodeConfig::load(&path);
assert!(res.is_err(), "expected deny_unknown_fields to reject");
}
fn tempdir() -> TempDir {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let pid = std::process::id();
let mut base = std::env::temp_dir();
base.push(format!("arknet-test-{pid}-{seq}"));
std::fs::create_dir_all(&base).unwrap();
TempDir { path: base }
}
struct TempDir {
path: std::path::PathBuf,
}
impl TempDir {
fn path(&self) -> &std::path::Path {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}