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 #[serde(default)]
61 pub da: DaSection,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct NodeSection {
68 pub name: String,
70 pub network: String,
72 pub data_dir: PathBuf,
74 pub log_level: String,
76}
77
78impl Default for NodeSection {
79 fn default() -> Self {
80 Self {
81 name: "arknet-node".into(),
82 network: "devnet".into(),
83 data_dir: PathBuf::from("/var/lib/arknet"),
84 log_level: "info".into(),
85 }
86 }
87}
88
89#[derive(Clone, Debug, Default, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct RolesSection {
93 #[serde(default)]
95 pub validator: bool,
96 #[serde(default)]
98 pub router: bool,
99 #[serde(default)]
101 pub compute: bool,
102 #[serde(default)]
104 pub verifier: bool,
105}
106
107#[derive(Clone, Debug, Default, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct ResourcesSection {
111 #[serde(default)]
113 pub compute: ComputeResources,
114 #[serde(default)]
116 pub router: RouterResources,
117 #[serde(default)]
119 pub verifier: VerifierResources,
120 #[serde(default)]
122 pub validator: ValidatorResources,
123}
124
125#[derive(Clone, Debug, Default, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct ComputeResources {
129 #[serde(default)]
131 pub gpu_devices: Vec<u32>,
132 #[serde(default)]
134 pub max_vram_gb: u32,
135 #[serde(default)]
137 pub max_concurrent_jobs: u32,
138 #[serde(default)]
140 pub loaded_models: Vec<String>,
141 #[serde(default)]
143 pub model_swap_enabled: bool,
144}
145
146#[derive(Clone, Debug, Default, Serialize, Deserialize)]
148#[serde(deny_unknown_fields)]
149pub struct RouterResources {
150 #[serde(default)]
152 pub cpu_percent: u32,
153 #[serde(default)]
155 pub max_concurrent_routes: u32,
156 #[serde(default)]
158 pub bandwidth_mbps: u32,
159}
160
161#[derive(Clone, Debug, Default, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct VerifierResources {
165 #[serde(default)]
167 pub gpu_share_with_compute: bool,
168 #[serde(default)]
170 pub max_verifications_per_hour: u32,
171}
172
173#[derive(Clone, Debug, Default, Serialize, Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct ValidatorResources {
177 #[serde(default)]
179 pub state_db_path: Option<PathBuf>,
180 #[serde(default)]
182 pub gossip_peers_max: u32,
183 #[serde(default)]
185 pub remote_signer: Option<String>,
186}
187
188#[derive(Clone, Debug, Default, Serialize, Deserialize)]
190#[serde(deny_unknown_fields)]
191pub struct OperatorSection {
192 #[serde(default)]
194 pub payout_address: Option<String>,
195 #[serde(default)]
197 pub preferred_region: Option<String>,
198 #[serde(default)]
200 pub min_reward_per_job: Option<String>,
201 #[serde(default)]
203 pub auto_model_pull: bool,
204 #[serde(default)]
206 pub auto_pool_rebalance: bool,
207}
208
209#[derive(Clone, Debug, Serialize, Deserialize)]
211#[serde(deny_unknown_fields)]
212pub struct NetworkSection {
213 pub p2p_listen: String,
215 pub rpc_listen: String,
217 pub metrics_listen: String,
219 #[serde(default)]
221 pub bootstrap_peers: Vec<String>,
222 #[serde(default)]
224 pub external_address: Option<String>,
225 #[serde(default)]
227 pub max_inbound_peers: u32,
228 #[serde(default)]
230 pub max_outbound_peers: u32,
231}
232
233impl Default for NetworkSection {
234 fn default() -> Self {
235 Self {
236 p2p_listen: "0.0.0.0:26656".into(),
237 rpc_listen: "127.0.0.1:26657".into(),
238 metrics_listen: "127.0.0.1:9090".into(),
239 bootstrap_peers: Vec::new(),
240 external_address: None,
241 max_inbound_peers: 60,
242 max_outbound_peers: 20,
243 }
244 }
245}
246
247#[derive(Clone, Debug, Default, Serialize, Deserialize)]
249#[serde(deny_unknown_fields)]
250pub struct TelemetrySection {
251 #[serde(default)]
253 pub prometheus_enabled: bool,
254 #[serde(default)]
256 pub otlp_endpoint: Option<String>,
257 #[serde(default)]
259 pub sentry_dsn: Option<String>,
260}
261
262#[derive(Clone, Debug, Default, Serialize, Deserialize)]
274#[serde(deny_unknown_fields)]
275pub struct TeeSection {
276 #[serde(default)]
278 pub enabled: bool,
279 #[serde(default)]
281 pub platform: Option<String>,
282 #[serde(default)]
284 pub enclave_key_path: Option<PathBuf>,
285}
286
287#[derive(Clone, Debug, Default, Serialize, Deserialize)]
289pub struct DaSection {
290 #[serde(default)]
292 pub layer: String,
293 #[serde(default)]
295 pub endpoint: String,
296 #[serde(default)]
298 pub namespace: String,
299 #[serde(default)]
301 pub auth_token: String,
302}
303
304impl NodeConfig {
305 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
307 Figment::new()
308 .merge(Serialized::defaults(NodeConfig::default()))
309 .merge(Toml::file(path.as_ref()))
310 .merge(Env::prefixed("ARKNET_").split("__"))
311 .extract::<Self>()
312 .map_err(|e| CommonError::Config(e.to_string()))
313 }
314
315 pub fn load_env_only() -> Result<Self> {
317 Figment::new()
318 .merge(Serialized::defaults(NodeConfig::default()))
319 .merge(Env::prefixed("ARKNET_").split("__"))
320 .extract::<Self>()
321 .map_err(|e| CommonError::Config(e.to_string()))
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn defaults_are_sensible() {
331 let c = NodeConfig::default();
332 assert_eq!(c.node.network, "devnet");
333 assert_eq!(c.node.log_level, "info");
334 assert!(!c.roles.validator);
335 assert!(!c.roles.router);
336 assert!(!c.roles.compute);
337 assert!(!c.roles.verifier);
338 assert_eq!(c.network.p2p_listen, "0.0.0.0:26656");
339 }
340
341 #[test]
342 fn loads_minimal_toml() {
343 let tmp = tempdir();
344 let path = tmp.path().join("node.toml");
345 std::fs::write(
346 &path,
347 r#"
348[node]
349name = "test-node"
350network = "testnet"
351data_dir = "/tmp/arknet-test"
352log_level = "debug"
353
354[roles]
355router = true
356compute = true
357"#,
358 )
359 .unwrap();
360
361 let c = NodeConfig::load(&path).expect("config loads");
362 assert_eq!(c.node.name, "test-node");
363 assert_eq!(c.node.network, "testnet");
364 assert!(c.roles.router);
365 assert!(c.roles.compute);
366 assert!(!c.roles.validator);
367 }
368
369 #[test]
370 fn rejects_unknown_fields() {
371 let tmp = tempdir();
372 let path = tmp.path().join("node.toml");
373 std::fs::write(
374 &path,
375 r#"
376[node]
377name = "x"
378network = "devnet"
379data_dir = "/tmp"
380log_level = "info"
381mystery_field = 42
382"#,
383 )
384 .unwrap();
385
386 let res = NodeConfig::load(&path);
387 assert!(res.is_err(), "expected deny_unknown_fields to reject");
388 }
389
390 fn tempdir() -> TempDir {
396 static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
397 let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
398 let pid = std::process::id();
399 let mut base = std::env::temp_dir();
400 base.push(format!("arknet-test-{pid}-{seq}"));
401 std::fs::create_dir_all(&base).unwrap();
402 TempDir { path: base }
403 }
404
405 struct TempDir {
406 path: std::path::PathBuf,
407 }
408
409 impl TempDir {
410 fn path(&self) -> &std::path::Path {
411 &self.path
412 }
413 }
414
415 impl Drop for TempDir {
416 fn drop(&mut self) {
417 let _ = std::fs::remove_dir_all(&self.path);
418 }
419 }
420}