1use std::path::{Path, PathBuf};
13
14use figment::{
15 providers::{Env, Format, Serialized, Toml},
16 Figment,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::errors::{CommonError, Result};
21
22#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct NodeConfig {
31 #[serde(default)]
33 pub node: NodeSection,
34
35 #[serde(default)]
37 pub roles: RolesSection,
38
39 #[serde(default)]
41 pub resources: ResourcesSection,
42
43 #[serde(default)]
45 pub operator: OperatorSection,
46
47 #[serde(default)]
49 pub network: NetworkSection,
50
51 #[serde(default)]
53 pub telemetry: TelemetrySection,
54
55 #[serde(default)]
57 pub tee: TeeSection,
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct NodeSection {
64 pub name: String,
66 pub network: String,
68 pub data_dir: PathBuf,
70 pub log_level: String,
72}
73
74impl Default for NodeSection {
75 fn default() -> Self {
76 Self {
77 name: "arknet-node".into(),
78 network: "devnet".into(),
79 data_dir: PathBuf::from("/var/lib/arknet"),
80 log_level: "info".into(),
81 }
82 }
83}
84
85#[derive(Clone, Debug, Default, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct RolesSection {
89 #[serde(default)]
91 pub validator: bool,
92 #[serde(default)]
94 pub router: bool,
95 #[serde(default)]
97 pub compute: bool,
98 #[serde(default)]
100 pub verifier: bool,
101}
102
103#[derive(Clone, Debug, Default, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct ResourcesSection {
107 #[serde(default)]
109 pub compute: ComputeResources,
110 #[serde(default)]
112 pub router: RouterResources,
113 #[serde(default)]
115 pub verifier: VerifierResources,
116 #[serde(default)]
118 pub validator: ValidatorResources,
119}
120
121#[derive(Clone, Debug, Default, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct ComputeResources {
125 #[serde(default)]
127 pub gpu_devices: Vec<u32>,
128 #[serde(default)]
130 pub max_vram_gb: u32,
131 #[serde(default)]
133 pub max_concurrent_jobs: u32,
134 #[serde(default)]
136 pub loaded_models: Vec<String>,
137 #[serde(default)]
139 pub model_swap_enabled: bool,
140}
141
142#[derive(Clone, Debug, Default, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct RouterResources {
146 #[serde(default)]
148 pub cpu_percent: u32,
149 #[serde(default)]
151 pub max_concurrent_routes: u32,
152 #[serde(default)]
154 pub bandwidth_mbps: u32,
155}
156
157#[derive(Clone, Debug, Default, Serialize, Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct VerifierResources {
161 #[serde(default)]
163 pub gpu_share_with_compute: bool,
164 #[serde(default)]
166 pub max_verifications_per_hour: u32,
167}
168
169#[derive(Clone, Debug, Default, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct ValidatorResources {
173 #[serde(default)]
175 pub state_db_path: Option<PathBuf>,
176 #[serde(default)]
178 pub gossip_peers_max: u32,
179 #[serde(default)]
181 pub remote_signer: Option<String>,
182}
183
184#[derive(Clone, Debug, Default, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub struct OperatorSection {
188 #[serde(default)]
190 pub payout_address: Option<String>,
191 #[serde(default)]
193 pub preferred_region: Option<String>,
194 #[serde(default)]
196 pub min_reward_per_job: Option<String>,
197 #[serde(default)]
199 pub auto_model_pull: bool,
200 #[serde(default)]
202 pub auto_pool_rebalance: bool,
203}
204
205#[derive(Clone, Debug, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct NetworkSection {
209 pub p2p_listen: String,
211 pub rpc_listen: String,
213 pub metrics_listen: String,
215 #[serde(default)]
217 pub bootstrap_peers: Vec<String>,
218 #[serde(default)]
220 pub external_address: Option<String>,
221 #[serde(default)]
223 pub max_inbound_peers: u32,
224 #[serde(default)]
226 pub max_outbound_peers: u32,
227}
228
229impl Default for NetworkSection {
230 fn default() -> Self {
231 Self {
232 p2p_listen: "0.0.0.0:26656".into(),
233 rpc_listen: "127.0.0.1:26657".into(),
234 metrics_listen: "127.0.0.1:9090".into(),
235 bootstrap_peers: Vec::new(),
236 external_address: None,
237 max_inbound_peers: 60,
238 max_outbound_peers: 20,
239 }
240 }
241}
242
243#[derive(Clone, Debug, Default, Serialize, Deserialize)]
245#[serde(deny_unknown_fields)]
246pub struct TelemetrySection {
247 #[serde(default)]
249 pub prometheus_enabled: bool,
250 #[serde(default)]
252 pub otlp_endpoint: Option<String>,
253 #[serde(default)]
255 pub sentry_dsn: Option<String>,
256}
257
258#[derive(Clone, Debug, Default, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct TeeSection {
272 #[serde(default)]
274 pub enabled: bool,
275 #[serde(default)]
277 pub platform: Option<String>,
278 #[serde(default)]
280 pub enclave_key_path: Option<PathBuf>,
281}
282
283impl NodeConfig {
284 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
286 Figment::new()
287 .merge(Serialized::defaults(NodeConfig::default()))
288 .merge(Toml::file(path.as_ref()))
289 .merge(Env::prefixed("ARKNET_").split("__"))
290 .extract::<Self>()
291 .map_err(|e| CommonError::Config(e.to_string()))
292 }
293
294 pub fn load_env_only() -> Result<Self> {
296 Figment::new()
297 .merge(Serialized::defaults(NodeConfig::default()))
298 .merge(Env::prefixed("ARKNET_").split("__"))
299 .extract::<Self>()
300 .map_err(|e| CommonError::Config(e.to_string()))
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn defaults_are_sensible() {
310 let c = NodeConfig::default();
311 assert_eq!(c.node.network, "devnet");
312 assert_eq!(c.node.log_level, "info");
313 assert!(!c.roles.validator);
314 assert!(!c.roles.router);
315 assert!(!c.roles.compute);
316 assert!(!c.roles.verifier);
317 assert_eq!(c.network.p2p_listen, "0.0.0.0:26656");
318 }
319
320 #[test]
321 fn loads_minimal_toml() {
322 let tmp = tempdir();
323 let path = tmp.path().join("node.toml");
324 std::fs::write(
325 &path,
326 r#"
327[node]
328name = "test-node"
329network = "testnet"
330data_dir = "/tmp/arknet-test"
331log_level = "debug"
332
333[roles]
334router = true
335compute = true
336"#,
337 )
338 .unwrap();
339
340 let c = NodeConfig::load(&path).expect("config loads");
341 assert_eq!(c.node.name, "test-node");
342 assert_eq!(c.node.network, "testnet");
343 assert!(c.roles.router);
344 assert!(c.roles.compute);
345 assert!(!c.roles.validator);
346 }
347
348 #[test]
349 fn rejects_unknown_fields() {
350 let tmp = tempdir();
351 let path = tmp.path().join("node.toml");
352 std::fs::write(
353 &path,
354 r#"
355[node]
356name = "x"
357network = "devnet"
358data_dir = "/tmp"
359log_level = "info"
360mystery_field = 42
361"#,
362 )
363 .unwrap();
364
365 let res = NodeConfig::load(&path);
366 assert!(res.is_err(), "expected deny_unknown_fields to reject");
367 }
368
369 fn tempdir() -> TempDir {
375 static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
376 let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
377 let pid = std::process::id();
378 let mut base = std::env::temp_dir();
379 base.push(format!("arknet-test-{pid}-{seq}"));
380 std::fs::create_dir_all(&base).unwrap();
381 TempDir { path: base }
382 }
383
384 struct TempDir {
385 path: std::path::PathBuf,
386 }
387
388 impl TempDir {
389 fn path(&self) -> &std::path::Path {
390 &self.path
391 }
392 }
393
394 impl Drop for TempDir {
395 fn drop(&mut self) {
396 let _ = std::fs::remove_dir_all(&self.path);
397 }
398 }
399}