Skip to main content

amadeus_node/
config.rs

1/// The only file where reading environment variables is allowed
2/// Basically, the config for the whole the core library
3use crate::node::anr::Anr;
4use crate::utils::bls12_381;
5pub use crate::utils::bls12_381::generate_sk as gen_sk;
6use crate::utils::ip_resolver::resolve_public_ipv4;
7use crate::utils::version::Ver;
8use crate::utils::{PublicKey, Signature};
9use serde::{Deserialize, Serialize};
10use std::net::Ipv4Addr;
11use std::path::Path;
12use tokio::fs;
13use tracing::{debug, info, warn};
14
15// constants from elixir config/config.exs
16pub const ENTRY_SIZE: usize = 524288; // 512 KiB
17pub const TX_SIZE: usize = 393216; // 384 KiB  
18pub const ATTESTATION_SIZE: usize = 512;
19pub const QUORUM: usize = 3; // quorum size for AMA
20pub const QUORUM_SINGLE: usize = 1; // quorum size for single shard
21pub const CLEANUP_PERIOD_MILLIS: u64 = 3000; // how often node does the cleanup
22pub const ANR_PERIOD_MILLIS: u64 = 3000; // how often node checks ANR status
23pub const BROADCAST_PERIOD_MILLIS: u64 = 500; // how often node broadcasts pings
24pub const AUTOUPDATE_PERIOD_MILLIS: u64 = 1000; // how often node checks for updates
25pub const CONSENSUS_PERIOD_MILLIS: u64 = 100; // how often node runs consensus
26pub const CATCHUP_PERIOD_MILLIS: u64 = 1000; // how often node checks for entry gaps
27
28pub const VERSION: Ver = parse_version();
29
30/// IMPORTANT for compatibility
31pub const BINCODE_CONFIG: bincode::config::Configuration<
32    bincode::config::BigEndian,
33    bincode::config::Fixint,
34    bincode::config::NoLimit,
35> = bincode::config::standard()
36    .with_fixed_int_encoding() // fixed ints
37    .with_big_endian() // network byte order
38    .with_no_limit(); // no size cap
39
40const fn parse_version() -> Ver {
41    const S: &str = env!("CRATE_VERSION");
42    let bytes = S.as_bytes();
43    let mut out = [0u8; 3];
44    let mut acc = 0u8;
45    let mut part = 0;
46    let mut i = 0;
47    while i < bytes.len() {
48        let b = bytes[i];
49        if b == b'.' {
50            out[part] = acc;
51            part += 1;
52            acc = 0;
53        } else {
54            acc = acc * 10 + (b - b'0');
55        }
56        i += 1;
57    }
58    out[part] = acc;
59    Ver::from_bytes(out)
60}
61
62pub const SEED_NODES: &[&str] = &["72.9.144.110", "167.235.169.185", "37.27.238.30"];
63
64// seed anr from elixir config/config.exs
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SeedANR {
67    pub ip4: String,
68    pub port: u16,
69    pub version: Ver,
70    pub signature: Vec<u8>,
71    pub pop: Signature,
72    pub ts: u32,
73    pub pk: PublicKey,
74}
75
76#[derive(Debug, thiserror::Error)]
77pub enum Error {
78    #[error(transparent)]
79    Io(#[from] std::io::Error),
80    #[error(transparent)]
81    Parse(#[from] std::net::AddrParseError),
82    #[error(transparent)]
83    B58(#[from] bs58::decode::Error),
84    #[error(transparent)]
85    Bls(#[from] bls12_381::Error),
86    #[error("invalid sk length: {0}, expected 64")]
87    InvalidSkLength(usize),
88    #[error(transparent)]
89    Anr(#[from] crate::node::anr::Error),
90}
91
92#[derive(Clone)]
93pub struct Config {
94    // filesystem paths
95    pub work_folder: String,
96
97    // version info
98    pub version: Ver,
99
100    // network configuration
101    pub offline: bool,
102    pub http_ipv4: Ipv4Addr,
103    pub http_port: u16,
104    pub udp_ipv4: Ipv4Addr,
105    pub udp_port: u16,
106    pub public_ipv4: Option<String>,
107
108    // node discovery
109    pub seed_ips: Vec<Ipv4Addr>,
110    pub seed_anrs: Vec<SeedANR>,
111    pub other_nodes: Vec<String>,
112    pub trust_factor: f64,
113    pub max_peers: usize,
114
115    // trainer keys
116    pub trainer_sk: [u8; 64],
117    pub trainer_pk: PublicKey,
118    pub trainer_pk_b58: String,
119    pub trainer_pop: Vec<u8>,
120
121    // runtime settings
122    pub archival_node: bool,
123    pub autoupdate: bool,
124    pub computor_type: Option<ComputorType>,
125    pub snapshot_height: u64,
126
127    // anr configuration
128    pub anr: Option<Anr>,
129    pub anr_name: Option<String>,
130    pub anr_desc: Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub enum ComputorType {
135    Trainer,
136    Default,
137}
138
139impl Config {
140    /// Generates pk from self.sk
141    pub fn get_pk(&self) -> PublicKey {
142        self.trainer_pk
143    }
144
145    pub fn get_sk(&self) -> [u8; 64] {
146        self.trainer_sk
147    }
148
149    pub fn get_pop(&self) -> Vec<u8> {
150        self.trainer_pop.clone()
151    }
152
153    /// Returns root work folder path
154    pub fn get_root(&self) -> &str {
155        &self.work_folder
156    }
157
158    pub fn get_ver(&self) -> Ver {
159        self.version
160    }
161
162    pub fn get_ver_3b(&self) -> (u8, u8, u8) {
163        (self.version.major(), self.version.minor(), self.version.patch())
164    }
165
166    pub fn get_public_ipv4(&self) -> Ipv4Addr {
167        self.public_ipv4
168            .as_ref()
169            .and_then(|s| s.parse::<Ipv4Addr>().ok())
170            .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1))
171    }
172
173    pub fn get_udp_port(&self) -> u16 {
174        self.udp_port
175    }
176
177    /// Create Config instance matching elixir config/runtime.exs
178    pub async fn from_fs(root: Option<&str>, sk: Option<&str>) -> Result<Self, Error> {
179        // work folder from env or default
180        let work_folder = std::env::var("WORKFOLDER").unwrap_or_else(|_| {
181            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
182            format!("{}/.amadeusd-rs", home)
183        });
184
185        // override with provided root if given
186        let work_folder = root.unwrap_or(&work_folder).to_string();
187        fs::create_dir_all(&work_folder).await?;
188
189        let version = VERSION;
190
191        // network configuration from env
192        let offline = std::env::var("OFFLINE").is_ok();
193        let http_ipv4 = std::env::var("HTTP_IPV4")
194            .unwrap_or_else(|_| "0.0.0.0".to_string())
195            .parse()
196            .unwrap_or(Ipv4Addr::new(0, 0, 0, 0));
197        let http_port = std::env::var("HTTP_PORT").unwrap_or_else(|_| "80".to_string()).parse().unwrap_or(80);
198        let udp_ipv4 = std::env::var("UDP_IPV4")
199            .unwrap_or_else(|_| "0.0.0.0".to_string())
200            .parse()
201            .unwrap_or(Ipv4Addr::new(0, 0, 0, 0));
202        let udp_port = 36969;
203
204        // node discovery
205        let seed_ips = SEED_NODES.iter().map(|s| s.parse()).collect::<Result<Vec<Ipv4Addr>, _>>()?;
206        let other_nodes =
207            std::env::var("OTHERNODES").map(|s| s.split(',').map(String::from).collect()).unwrap_or_else(|_| vec![]);
208        let trust_factor = std::env::var("TRUSTFACTOR").ok().and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.8);
209        let max_peers = std::env::var("MAX_PEERS").unwrap_or_else(|_| "300".to_string()).parse().unwrap_or(300);
210
211        // verify time sync (warning only)
212        if !verify_time_sync() {
213            warn!("time not synced OR systemd-ntp client not found (can cause sync errors)");
214        }
215
216        // load or generate trainer keys
217        let default_sk_path = format!("{}/sk", work_folder);
218        let (trainer_sk, trainer_pk, trainer_pk_b58) = if let Some(path) = sk {
219            let sk = read_sk(path).await?;
220            let pk = get_pk(&sk);
221            (sk, pk, bs58::encode(pk).into_string())
222        } else if let Ok(sk) = read_sk(&default_sk_path).await {
223            let pk = get_pk(&sk);
224            (sk, pk, bs58::encode(pk).into_string())
225        } else {
226            debug!("no sk (BLS12-381) found, generating new trainer keys");
227            let sk = gen_sk();
228            let pk = get_pk(&sk);
229            let pk_b58 = bs58::encode(pk).into_string();
230            info!("generated random sk at {default_sk_path}, pk {pk_b58}");
231            write_sk(&default_sk_path, sk).await?;
232            (sk, pk, pk_b58)
233        };
234
235        // generate proof of possession
236        let trainer_pop = bls12_381::sign(&trainer_sk, trainer_pk.as_ref(), crate::consensus::DST_POP)
237            .map(|sig| sig.to_vec())
238            .unwrap_or_else(|_| vec![0u8; 96]);
239
240        // runtime settings from env
241        let archival_node = matches!(std::env::var("ARCHIVALNODE").as_deref(), Ok("true") | Ok("y") | Ok("yes"));
242        let autoupdate = matches!(std::env::var("AUTOUPDATE").as_deref(), Ok("true") | Ok("y") | Ok("yes"));
243        let computor_type = match std::env::var("COMPUTOR").as_deref() {
244            Ok("trainer") => Some(ComputorType::Trainer),
245            Ok(_) => Some(ComputorType::Default),
246            Err(_) => None,
247        };
248        let snapshot_height =
249            std::env::var("SNAPSHOT_HEIGHT").unwrap_or_else(|_| "24875547".to_string()).parse().unwrap_or(24875547);
250
251        // get public IP
252        let my_ip = match std::env::var("PUBLIC_UDP_IPV4").ok().and_then(|i| i.parse::<Ipv4Addr>().ok()) {
253            Some(ip) => Some(ip),
254            None => resolve_public_ipv4().await, //.unwrap_or(Ipv4Addr::new(127, 0, 0, 1)),
255        };
256
257        let ver = version;
258        let public_ipv4 = my_ip.map(|ip| ip.to_string());
259
260        // anr configuration
261        let anr_name = std::env::var("ANR_NAME").ok();
262        let anr_desc = std::env::var("ANR_DESC").ok();
263
264        let anr = my_ip.and_then(|ip| {
265            Anr::build_with_name_desc(
266                &trainer_sk,
267                &trainer_pk,
268                &trainer_pop,
269                ip,
270                ver,
271                anr_name.clone(),
272                anr_desc.clone(),
273            )
274            .ok()
275        });
276
277        let seed_anrs = vec![
278            SeedANR {
279                ip4: "72.9.144.110".into(),
280                port: 36969,
281                version: "1.2.5".try_into().unwrap(),
282                signature: vec![
283                    132, 66, 55, 44, 126, 255, 89, 218, 33, 243, 214, 44, 103, 181, 166, 16, 54, 195, 44, 159, 66, 87,
284                    162, 103, 240, 217, 235, 201, 120, 234, 207, 126, 31, 24, 228, 115, 202, 68, 85, 167, 97, 101, 110,
285                    201, 220, 161, 239, 95, 15, 174, 251, 185, 62, 16, 69, 231, 99, 94, 189, 94, 210, 122, 150, 220,
286                    226, 218, 207, 108, 242, 168, 189, 252, 146, 129, 113, 239, 53, 211, 69, 92, 115, 218, 40, 15, 104,
287                    220, 235, 59, 235, 8, 177, 105, 120, 225, 12, 87,
288                ],
289                pop: Signature::from([
290                    182, 42, 150, 214, 42, 240, 210, 215, 0, 106, 181, 96, 198, 75, 222, 86, 45, 241, 58, 230, 66, 56,
291                    10, 49, 217, 53, 39, 100, 18, 197, 159, 153, 68, 220, 234, 164, 6, 9, 3, 228, 234, 209, 151, 233,
292                    122, 209, 101, 73, 16, 190, 135, 172, 85, 106, 80, 99, 225, 214, 141, 245, 66, 170, 177, 163, 247,
293                    93, 243, 234, 184, 145, 167, 202, 181, 114, 186, 113, 112, 113, 108, 84, 135, 24, 62, 242, 142,
294                    248, 159, 124, 117, 85, 190, 43, 177, 212, 18, 24,
295                ]),
296                ts: 1763051924,
297                pk: PublicKey::from([
298                    169, 232, 30, 216, 200, 234, 174, 189, 141, 213, 58, 136, 157, 140, 90, 134, 18, 171, 115, 48, 39,
299                    90, 93, 57, 4, 62, 149, 32, 14, 124, 27, 102, 240, 220, 0, 197, 48, 126, 134, 122, 85, 169, 173,
300                    158, 122, 228, 185, 240,
301                ]),
302            },
303            SeedANR {
304                ip4: "167.235.169.185".into(),
305                port: 36969,
306                version: "1.2.5".try_into().unwrap(),
307                signature: vec![
308                    128, 163, 49, 163, 123, 191, 81, 52, 48, 69, 65, 212, 233, 85, 88, 167, 138, 156, 204, 105, 91,
309                    231, 234, 219, 129, 161, 5, 226, 136, 179, 43, 192, 136, 224, 78, 114, 134, 157, 142, 36, 236, 10,
310                    83, 4, 102, 209, 48, 133, 0, 116, 212, 250, 22, 218, 156, 26, 50, 86, 197, 56, 110, 252, 252, 108,
311                    202, 58, 218, 181, 30, 213, 227, 46, 175, 227, 45, 174, 162, 33, 160, 113, 165, 114, 115, 232, 207,
312                    65, 111, 11, 213, 110, 195, 70, 197, 199, 176, 241,
313                ],
314                pop: Signature::from([
315                    164, 246, 242, 104, 121, 244, 113, 37, 125, 250, 253, 128, 164, 91, 172, 179, 127, 196, 28, 189,
316                    170, 239, 154, 195, 216, 36, 42, 27, 69, 132, 140, 126, 88, 213, 175, 124, 0, 109, 83, 10, 90, 12,
317                    56, 188, 226, 219, 163, 219, 6, 28, 138, 155, 129, 47, 145, 171, 20, 95, 100, 57, 188, 175, 139,
318                    53, 129, 60, 97, 54, 239, 1, 154, 113, 121, 134, 234, 154, 63, 48, 187, 88, 153, 84, 159, 97, 129,
319                    241, 120, 192, 107, 55, 45, 250, 208, 196, 44, 141,
320                ]),
321                ts: 1763065634,
322                pk: PublicKey::from([
323                    176, 120, 75, 148, 69, 45, 127, 232, 195, 254, 164, 173, 136, 138, 157, 97, 246, 81, 114, 66, 48,
324                    199, 162, 230, 152, 219, 207, 224, 18, 246, 194, 150, 228, 105, 126, 41, 68, 36, 246, 51, 135, 216,
325                    105, 117, 245, 98, 93, 140,
326                ]),
327            },
328            SeedANR {
329                ip4: "37.27.238.30".into(),
330                port: 36969,
331                version: "1.2.5".try_into().unwrap(),
332                signature: vec![
333                    142, 128, 166, 98, 166, 146, 49, 71, 166, 160, 208, 182, 120, 22, 114, 79, 95, 23, 30, 33, 120, 68,
334                    246, 221, 77, 117, 120, 68, 108, 2, 103, 47, 105, 210, 24, 12, 30, 153, 249, 57, 20, 188, 253, 203,
335                    90, 37, 214, 84, 9, 155, 80, 94, 127, 0, 67, 16, 11, 17, 24, 86, 230, 23, 63, 13, 39, 139, 63, 123,
336                    5, 83, 154, 38, 183, 44, 1, 66, 112, 7, 53, 155, 129, 58, 239, 68, 155, 115, 239, 241, 255, 65, 56,
337                    0, 110, 110, 137, 27,
338                ],
339                pop: Signature::from([
340                    181, 170, 103, 46, 138, 14, 103, 212, 122, 113, 28, 78, 228, 170, 19, 202, 108, 149, 207, 132, 49,
341                    208, 221, 32, 121, 229, 5, 95, 72, 120, 73, 47, 230, 70, 155, 89, 98, 194, 196, 206, 230, 63, 49,
342                    205, 160, 135, 90, 246, 7, 93, 79, 237, 127, 111, 189, 136, 122, 225, 135, 54, 45, 95, 36, 186,
343                    211, 155, 67, 24, 86, 151, 91, 190, 20, 72, 145, 77, 138, 55, 22, 15, 100, 61, 140, 137, 93, 55,
344                    19, 5, 226, 106, 30, 134, 74, 55, 193, 163,
345                ]),
346                ts: 1763066818,
347                pk: PublicKey::from([
348                    149, 216, 55, 255, 29, 8, 239, 251, 139, 112, 30, 29, 199, 57, 90, 67, 198, 220, 101, 18, 228, 100,
349                    100, 241, 43, 213, 221, 230, 253, 58, 231, 1, 102, 166, 54, 66, 245, 148, 140, 44, 78, 56, 84, 12,
350                    222, 205, 57, 210,
351                ]),
352            },
353        ];
354
355        Ok(Self {
356            work_folder,
357            version,
358            offline,
359            http_ipv4,
360            http_port,
361            udp_ipv4,
362            udp_port,
363            public_ipv4,
364            seed_ips,
365            seed_anrs,
366            other_nodes,
367            trust_factor,
368            max_peers,
369            trainer_sk,
370            trainer_pk,
371            trainer_pk_b58,
372            trainer_pop,
373            archival_node,
374            autoupdate,
375            computor_type,
376            snapshot_height,
377            anr,
378            anr_name,
379            anr_desc,
380        })
381    }
382
383    /// Get public IP asynchronously if not already set
384    pub async fn ensure_public_ip(&mut self) {
385        if self.public_ipv4.is_none() {
386            self.public_ipv4 = crate::utils::ip_resolver::resolve_public_ipv4_string().await;
387        }
388    }
389
390    pub fn new_daemonless(sk: [u8; 64]) -> Self {
391        let pk = get_pk(&sk);
392        let pk_b58 = bs58::encode(pk).into_string();
393        let pop = bls12_381::sign(&sk, pk.as_ref(), crate::consensus::DST_POP)
394            .map(|sig| sig.to_vec())
395            .unwrap_or_else(|_| vec![0u8; 96]);
396        let seed_ips = SEED_NODES.iter().map(|s| s.parse()).collect::<Result<Vec<Ipv4Addr>, _>>().unwrap_or_default();
397
398        Self {
399            work_folder: ".amadeusd-rs".to_string(),
400            version: VERSION,
401            offline: false,
402            http_ipv4: Ipv4Addr::new(0, 0, 0, 0),
403            http_port: 80,
404            udp_ipv4: Ipv4Addr::new(0, 0, 0, 0),
405            udp_port: 36969,
406            public_ipv4: None,
407            seed_ips,
408            seed_anrs: vec![],
409            other_nodes: vec![],
410            trust_factor: 0.8,
411            max_peers: 300,
412            trainer_sk: sk,
413            trainer_pk: pk,
414            trainer_pk_b58: pk_b58,
415            trainer_pop: pop,
416            archival_node: false,
417            autoupdate: false,
418            computor_type: None,
419            snapshot_height: 24875547,
420            anr: None,
421            anr_name: None,
422            anr_desc: None,
423        }
424    }
425}
426
427pub fn get_pk(sk: &[u8; 64]) -> PublicKey {
428    bls12_381::get_public_key(sk).expect("key generation should not fail with proper key material")
429}
430
431pub async fn write_sk(path: impl AsRef<Path>, sk: [u8; 64]) -> Result<(), Error> {
432    let sk_b58 = bs58::encode(sk).into_string();
433    fs::write(path, sk_b58).await.map_err(Into::into)
434}
435
436pub async fn read_sk(path: impl AsRef<Path>) -> Result<[u8; 64], Error> {
437    let sk_bs58 = fs::read_to_string(path).await?;
438    let sk_vec = bs58::decode(sk_bs58.trim()).into_vec()?;
439    sk_vec.try_into().map_err(|v: Vec<u8>| Error::InvalidSkLength(v.len()))
440}
441
442/// Verify time sync using systemd-timesyncd
443fn verify_time_sync() -> bool {
444    use std::process::Command;
445
446    // try to check systemd-timesyncd status like elixir
447    match Command::new("timedatectl").arg("status").output() {
448        Ok(output) => {
449            let stdout = String::from_utf8_lossy(&output.stdout);
450            // check if ntp is synchronized
451            stdout.contains("System clock synchronized: yes") || stdout.contains("NTP synchronized: yes")
452        }
453        Err(_) => {
454            // if timedatectl is not available, assume time is ok
455            true
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use std::any::type_name_of_val;
464    use std::path::PathBuf;
465    use std::time::{SystemTime, UNIX_EPOCH};
466
467    /// Guard that creates a per-test directory under /tmp and deletes it on drop.
468    struct TmpTestDir {
469        path: PathBuf,
470    }
471
472    impl TmpTestDir {
473        /// Create a tmp directory named "/tmp/<fully-qualified-test-path><seconds-since-epoch>".
474        /// Pass a reference to the test function item, e.g., `TmpTestDir::for_test(&my_test_fn)`.
475        fn for_test<F: ?Sized>(f: &F) -> std::io::Result<Self> {
476            let fq = type_name_of_val(f);
477            let secs = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
478            let dir_name = format!("{}{}", fq, secs);
479            let path = std::path::Path::new("/tmp").join(dir_name);
480            std::fs::create_dir_all(&path)?;
481            Ok(Self { path })
482        }
483
484        /// Convenience to get &str path.
485        fn to_str(&self) -> &str {
486            self.path.to_str().unwrap_or("")
487        }
488    }
489
490    impl Drop for TmpTestDir {
491        fn drop(&mut self) {
492            // best-effort cleanup
493            let _ = std::fs::remove_dir_all(&self.path);
494        }
495    }
496
497    #[tokio::test]
498    async fn test_config_from_env() {
499        // per-test tmp dir
500        let tmp = TmpTestDir::for_test(&test_config_from_env).unwrap();
501        // set up test environment
502        unsafe {
503            std::env::set_var("WORKFOLDER", tmp.to_str());
504            std::env::set_var("HTTP_PORT", "8080");
505            std::env::set_var("OTHERNODES", "192.168.1.1,192.168.1.2");
506            std::env::set_var("TRUSTFACTOR", "0.9");
507            std::env::set_var("MAX_PEERS", "500");
508            std::env::set_var("ARCHIVALNODE", "true");
509            std::env::set_var("AUTOUPDATE", "yes");
510            std::env::set_var("COMPUTOR", "trainer");
511            std::env::set_var("SNAPSHOT_HEIGHT", "12345678");
512            std::env::set_var("ANR_NAME", "TestNode");
513            std::env::set_var("ANR_DESC", "Test Description");
514        }
515
516        let config = Config::from_fs(Some(tmp.to_str()), None).await.unwrap();
517
518        // verify filesystem paths from test setup
519        assert_eq!(config.work_folder, tmp.to_str());
520
521        // verify network configuration from env
522        assert_eq!(config.http_port, 8080);
523        assert_eq!(config.other_nodes, vec!["192.168.1.1", "192.168.1.2"]);
524        assert_eq!(config.trust_factor, 0.9);
525        assert_eq!(config.max_peers, 500);
526
527        // verify generated trainer keys exist
528        assert_eq!(config.trainer_sk.len(), 64);
529        assert_eq!(config.trainer_pk.len(), 48);
530        assert!(!config.trainer_pk_b58.is_empty());
531        assert_eq!(config.trainer_pop.len(), 96);
532
533        // verify runtime settings from env
534        assert!(config.archival_node);
535        assert!(config.autoupdate);
536        assert_eq!(config.computor_type, Some(ComputorType::Trainer));
537        assert_eq!(config.snapshot_height, 12345678);
538
539        // verify anr configuration from env
540        assert_eq!(config.anr_name, Some("TestNode".to_string()));
541        assert_eq!(config.anr_desc, Some("Test Description".to_string()));
542    }
543
544    #[tokio::test]
545    async fn test_config_from_sk() {
546        let sk = [42u8; 64];
547        let config = Config::new_daemonless(sk);
548
549        // verify the provided sk is used
550        assert_eq!(config.trainer_sk, sk);
551
552        // verify keys are generated correctly
553        assert_eq!(config.trainer_pk.len(), 48);
554        assert!(!config.trainer_pk_b58.is_empty());
555        assert_eq!(config.trainer_pop.len(), 96);
556    }
557
558    #[tokio::test]
559    async fn test_config_env_parsing() {
560        // per-test tmp dir
561        let tmp = TmpTestDir::for_test(&test_config_env_parsing).unwrap();
562        // explicitly set and verify computor type parsing to avoid env races
563        unsafe {
564            std::env::set_var("COMPUTOR", "trainer");
565        }
566        let config = Config::from_fs(Some(tmp.to_str()), None).await.unwrap();
567        assert_eq!(config.computor_type, Some(ComputorType::Trainer));
568
569        unsafe {
570            std::env::set_var("COMPUTOR", "default");
571        }
572        let config = Config::from_fs(Some(tmp.to_str()), None).await.unwrap();
573        assert_eq!(config.computor_type, Some(ComputorType::Default));
574    }
575
576    #[tokio::test]
577    async fn test_config_version_methods() {
578        // per-test tmp dir
579        let tmp = TmpTestDir::for_test(&test_config_version_methods).unwrap();
580        let config = Config::from_fs(Some(tmp.to_str()), None).await.unwrap();
581
582        // Test that get_ver() returns a string and get_ver_3b() returns consistent tuple
583        let version_str = config.get_ver().to_string();
584        let version_3b = config.get_ver_3b();
585
586        // Parse the string version and compare with tuple
587        let parts: Vec<&str> = version_str.split('.').collect();
588        assert_eq!(parts.len(), 3, "Version string should have 3 parts");
589
590        let expected_major = parts[0].parse::<u8>().unwrap();
591        let expected_minor = parts[1].parse::<u8>().unwrap();
592        let expected_patch = parts[2].parse::<u8>().unwrap();
593
594        assert_eq!(version_3b.0, expected_major);
595        assert_eq!(version_3b.1, expected_minor);
596        assert_eq!(version_3b.2, expected_patch);
597
598        // Verify it matches the version field directly
599        assert_eq!(version_3b, (config.version.major(), config.version.minor(), config.version.patch()));
600    }
601
602    #[test]
603    fn create_keypair() {
604        let sk = gen_sk();
605        let pk = get_pk(&sk);
606        let pk_b58 = bs58::encode(pk).into_string();
607        let sk_b58 = bs58::encode(sk).into_string();
608        println!("generated random sk at {sk_b58}, pk {pk_b58}");
609    }
610}