kinetic-core 0.2.0

Core daemon primitives, VDF management, and network utilities for the Kinetic Network.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! Global configuration models, default values, and port definitions for Kinetic.
//!
//! This module defines `KineticConfig`, which represents the complete runtime
//! configuration loaded from disk (`config.toml`) or environment variables.
//!
//! ## Configuration Resolution Order
//!
//! 1. **Explicit file path**: `KINETIC_CONFIG_PATH` environment variable.
//! 2. **Default user path**: `~/.local/share/{NETWORK_ID}/config.toml` (or platform equivalent via `get_base_dir`).
//! 3. **Fallback defaults**: If the file does not exist, a clean default config is automatically written to disk.
//!
//! ## Port Allocation Strategy
//!
//! All default port assignments are centralized in the `ports` submodule to ensure
//! zero collisions between `kinetic-daemon`, `kinetic-node`, and `kinetic-host`.

use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use std::fs;
use std::path::PathBuf;
/// Maximum age in seconds (10 minutes) for cached host routing records in proxy forwarding.
/// Well-known default network port assignments for Kinetic binaries.
///
/// Centralizing port assignments here prevents accidental conflicts
/// between the daemon, node, and host processes on a single system.
pub mod ports {
    /// Default P2P listen port for `kinetic-daemon` (6070).
    pub const P2P_DAEMON: u16 = 6070;
    /// Default P2P listen port for `kinetic-node` (6071).
    pub const P2P_NODE: u16 = 6071;
    /// Default P2P listen port for `kinetic-host` (6072).
    pub const P2P_HOST: u16 = 6072;

    /// Default authenticated HTTP API port for `kinetic-daemon` (16002).
    pub const API_DAEMON: u16 = 16002;
    /// Default HTTP health-check port for `kinetic-node` (16003).
    pub const API_NODE: u16 = 16003;
    /// Default HTTP health-check port for `kinetic-host` (16004).
    pub const API_HOST: u16 = 16004;

    /// Default HTTP reverse-proxy port for intercepting `.kin` requests (17001).
    pub const PROXY: u16 = 17001;
    /// Default UDP DNS resolver port for native OS queries (53).
    pub const DNS: u16 = 53;
    /// Default local backend HTTP port (80).
    pub const BACKEND: u16 = 80;
    /// Default Proxy Auto-Config (PAC) server port (16001).
    pub const PAC: u16 = 16001;
}

/// Primary configuration container for Kinetic nodes and daemons.
///
/// Holds settings for daemon behavior, P2P networking, and Drand beacon connections.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KineticConfig {
    /// Daemon-level settings: ports, storage path, and network mode.
    pub daemon: DaemonConfig,
    /// P2P networking settings: ports, bootstrap nodes, and mDNS.
    pub network: P2pConfig,
    /// Drand randomness beacon settings: custom endpoints and DNS seed.
    #[serde(default)]
    pub drand: DrandConfig,
}

/// Drand networking configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrandConfig {
    /// Drand HTTP endpoints to query for Quicknet kyns.
    #[serde(
        default = "default_drand_endpoints",
        skip_serializing_if = "is_default_drand_endpoints"
    )]
    pub endpoints: Vec<String>,
    /// Domains to query via DNS TXT records for dynamic Drand endpoints.
    #[serde(default = "default_drand_seed_domain")]
    pub drand_domain: Vec<String>,
    /// If true, the node will only listen to P2P gossipsub for Drand kyns
    /// and will not query the internet via HTTP/DNS.
    #[serde(default)]
    pub p2p_only: bool,
}

fn default_drand_endpoints() -> Vec<String> {
    crate::constants::DRAND_HTTP_ENDPOINTS
        .iter()
        .map(|s| s.to_string())
        .collect()
}

fn is_default_drand_endpoints(val: &Vec<String>) -> bool {
    val == &default_drand_endpoints()
}

fn default_drand_seed_domain() -> Vec<String> {
    vec![format!("drand.{}", crate::constants::BASE_DOMAIN)]
}

impl Default for DrandConfig {
    fn default() -> Self {
        Self {
            endpoints: default_drand_endpoints(),
            drand_domain: default_drand_seed_domain(),
            p2p_only: false,
        }
    }
}

/// Daemon-specific configuration: API ports, storage paths, and operating mode.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonConfig {
    /// Local IP address to bind to for daemon services.
    #[serde(default = "local_bind_ip", skip_serializing_if = "is_default_bind_ip")]
    pub bind_ip: String,
    /// IP address used by the PAC script and the proxy.
    #[serde(default = "default_pac_bind_ip")]
    pub pac_bind_ip: String,
    /// Port for the daemon's authenticated HTTP API (default: [`ports::API_DAEMON`]).
    #[serde(
        default = "default_api_port",
        skip_serializing_if = "is_default_api_port"
    )]
    pub api_port: u16,
    /// Port for the built-in DNS resolver (default: [`ports::DNS`]).
    #[serde(default = "default_dns_port")]
    pub dns_port: u16,
    /// Port for the built-in HTTP reverse proxy (default: [`ports::PROXY`]).
    #[serde(default = "default_proxy_port")]
    pub proxy_port: u16,
    /// Port for the local backend HTTP server (default: [`ports::BACKEND`]).
    #[serde(
        default = "default_backend_port",
        skip_serializing_if = "is_default_backend_port"
    )]
    pub backend_port: u16,
    /// Whether to start the built-in UDP DNS resolver on boot (default: `true`).
    #[serde(default = "default_true")]
    pub enable_dns: bool,
    /// Path to the directory where the embedded storage database is persisted.
    pub storage_dir: PathBuf,
    /// Network operating mode. Supported values: `"FullNode"` (participates in DHT storage & routing)
    /// or `"LightNode"` (queries network without storing records).
    #[serde(default = "default_network_mode")]
    pub network_mode: String,
    /// Whether the node should automatically download and install OTA binary updates.
    #[serde(default = "default_auto_update")]
    pub auto_update: bool,
    /// Port for the PAC (Proxy Auto-Config) server (default: [`ports::PAC`]).
    #[serde(
        default = "default_pac_port",
        skip_serializing_if = "is_default_pac_port"
    )]
    pub pac_port: u16,
    /// IPFS gateway URL used to resolve `IPFS(cid)` records in the HTTP Proxy.
    #[serde(default = "default_ipfs_gateway")]
    pub ipfs_gateway: String,
    /// UDP port for querying the Kinetic Atlas Bridge daemon (default: `34291`).
    #[serde(default = "default_atlas_port")]
    pub atlas_port: u16,
}

fn local_bind_ip() -> String {
    crate::constants::LOCAL_BIND_IP.to_string()
}

fn default_pac_bind_ip() -> String {
    crate::constants::LOCAL_BIND_IP.to_string()
}

fn is_default_bind_ip(val: &String) -> bool {
    val == crate::constants::LOCAL_BIND_IP
}

fn default_true() -> bool {
    true
}

fn default_auto_update() -> bool {
    true
}

fn default_network_mode() -> String {
    "FullNode".to_string()
}

fn default_api_port() -> u16 {
    ports::API_DAEMON
}

fn default_dns_port() -> u16 {
    ports::DNS
}

fn default_proxy_port() -> u16 {
    ports::PROXY
}

fn default_backend_port() -> u16 {
    ports::BACKEND
}

fn default_pac_port() -> u16 {
    ports::PAC
}

fn is_default_api_port(val: &u16) -> bool {
    *val == ports::API_DAEMON
}
fn is_default_backend_port(val: &u16) -> bool {
    *val == ports::BACKEND
}
fn is_default_pac_port(val: &u16) -> bool {
    *val == ports::PAC
}

fn default_atlas_port() -> u16 {
    34291
}

fn default_ipfs_gateway() -> String {
    crate::constants::IPFS_GATEWAY.to_string()
}

/// P2P networking configuration shared across all Kinetic binaries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct P2pConfig {
    /// P2P listen port for the daemon (default: [`ports::P2P_DAEMON`]).
    #[serde(
        default = "default_p2p_daemon",
        skip_serializing_if = "is_default_p2p_daemon"
    )]
    pub daemon_port: u16,
    /// P2P listen port for the daemon over QUIC (default: [`ports::P2P_DAEMON`]).
    #[serde(
        default = "default_p2p_daemon_quic",
        skip_serializing_if = "is_default_p2p_daemon_quic"
    )]
    pub daemon_quic_port: u16,
    /// P2P listen port for the node (default: [`ports::P2P_NODE`]).
    #[serde(
        default = "default_p2p_node",
        skip_serializing_if = "is_default_p2p_node"
    )]
    pub node_port: u16,
    /// P2P listen port for the node over QUIC (default: [`ports::P2P_NODE`]).
    #[serde(
        default = "default_p2p_node_quic",
        skip_serializing_if = "is_default_p2p_node_quic"
    )]
    pub node_quic_port: u16,
    /// P2P listen port for the host (default: [`ports::P2P_HOST`]).
    #[serde(
        default = "default_p2p_host",
        skip_serializing_if = "is_default_p2p_host"
    )]
    pub host_port: u16,
    /// P2P listen port for the host over QUIC (default: [`ports::P2P_HOST`]).
    #[serde(
        default = "default_p2p_host_quic",
        skip_serializing_if = "is_default_p2p_host_quic"
    )]
    pub host_quic_port: u16,
    /// Multiaddr strings for the initial bootstrap peers.
    pub bootstrap_nodes: Vec<String>,
    /// `.kin` domain names used to discover additional bootstrap peers via DNS.
    #[serde(default)]
    pub seed_domain: Vec<String>,
    /// Whether to enable mDNS peer discovery on the local network.
    #[serde(default = "default_true")]
    pub enable_mdns: bool,
    /// Optional externally reachable multiaddr (e.g. for nodes behind NAT).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub external_address: Option<String>,
}

fn default_p2p_daemon() -> u16 {
    ports::P2P_DAEMON
}

fn default_p2p_daemon_quic() -> u16 {
    ports::P2P_DAEMON
}

fn default_p2p_node() -> u16 {
    ports::P2P_NODE
}

fn default_p2p_node_quic() -> u16 {
    ports::P2P_NODE
}

fn default_p2p_host() -> u16 {
    ports::P2P_HOST
}

fn default_p2p_host_quic() -> u16 {
    ports::P2P_HOST
}

fn is_default_p2p_daemon(val: &u16) -> bool {
    *val == ports::P2P_DAEMON
}
fn is_default_p2p_daemon_quic(val: &u16) -> bool {
    *val == ports::P2P_DAEMON
}
fn is_default_p2p_node(val: &u16) -> bool {
    *val == ports::P2P_NODE
}
fn is_default_p2p_node_quic(val: &u16) -> bool {
    *val == ports::P2P_NODE
}
fn is_default_p2p_host(val: &u16) -> bool {
    *val == ports::P2P_HOST
}
fn is_default_p2p_host_quic(val: &u16) -> bool {
    *val == ports::P2P_HOST
}

impl Default for KineticConfig {
    fn default() -> Self {
        #[cfg(not(target_arch = "wasm32"))]
        let storage_dir = crate::config::get_base_dir().join("db");

        #[cfg(target_arch = "wasm32")]
        let storage_dir = PathBuf::from("/kinetic-db");

        Self {
            daemon: DaemonConfig {
                bind_ip: crate::constants::LOCAL_BIND_IP.to_string(),
                pac_bind_ip: crate::constants::LOCAL_BIND_IP.to_string(),
                api_port: ports::API_DAEMON,
                dns_port: ports::DNS,
                proxy_port: ports::PROXY,
                backend_port: ports::BACKEND,
                enable_dns: true,
                storage_dir,
                network_mode: "FullNode".to_string(),
                auto_update: true,
                pac_port: ports::PAC,
                ipfs_gateway: crate::constants::IPFS_GATEWAY.to_string(),
                atlas_port: 34291,
            },
            network: P2pConfig {
                daemon_port: ports::P2P_DAEMON,
                daemon_quic_port: ports::P2P_DAEMON,
                node_port: ports::P2P_NODE,
                node_quic_port: ports::P2P_NODE,
                host_port: ports::P2P_HOST,
                host_quic_port: ports::P2P_HOST,
                bootstrap_nodes: crate::constants::BOOTSTRAP_NODES
                    .iter()
                    .map(|s| s.to_string())
                    .collect(),
                seed_domain: vec![format!("seed.{}", crate::constants::BASE_DOMAIN)],
                enable_mdns: true,
                external_address: None,
            },
            drand: DrandConfig::default(),
        }
    }
}

impl KineticConfig {
    /// Loads runtime configuration from disk (`config.toml`) or environment variables.
    ///
    /// Resolution Order:
    /// 1. Checks `KINETIC_CONFIG_PATH` environment variable.
    /// 2. Defaults to `get_base_dir().join("config.toml")`.
    /// 3. If missing, writes default configuration to disk and returns default settings.
    ///
    /// # Security & Fail-Closed Behavior
    ///
    /// If `config.toml` exists but contains invalid TOML syntax or corrupted fields, this method
    /// logs a critical error and aborts execution via `std::process::exit(1)`. This prevents
    /// "fail-open" security vulnerabilities where invalid configs silently degrade to insecure defaults.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn load() -> Self {
        let config_path = std::env::var(crate::constants::ENV_CONFIG_PATH)
            .map(PathBuf::from)
            .unwrap_or_else(|_| crate::config::get_base_dir().join("config.toml"));

        let config = match fs::read_to_string(&config_path) {
            Ok(config_str) => match toml::from_str(&config_str) {
                Ok(config) => config,
                Err(e) => {
                    tracing::error!("Failed to parse config.toml: {}. Refusing to start to avoid fail-open vulnerability.", e);
                    std::process::exit(1);
                }
            },
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                // Create default config only if it doesn't exist
                let default_cfg = Self::default();
                if let Some(parent) = config_path.parent() {
                    let _ = fs::create_dir_all(parent);
                    if let Ok(toml_str) = toml::to_string_pretty(&default_cfg) {
                        let _ = fs::write(&config_path, toml_str);
                    }
                }
                default_cfg
            }
            Err(e) => {
                tracing::error!("Failed to read config.toml: {}. Refusing to start to avoid fail-open vulnerability.", e);
                std::process::exit(1);
            }
        };

        config
    }

    #[cfg(target_arch = "wasm32")]
    /// Stub implementation for loading configuration in Wasm environments.
    pub fn load() -> Self {
        Self::default()
    }

    /// Serializes and writes the current configuration back to `config.toml`.
    ///
    /// # Errors
    ///
    /// Returns [`std::io::Error`] if file creation, TOML serialization, or writing fails.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn save(&self) -> Result<(), std::io::Error> {
        let config_path = std::env::var(crate::constants::ENV_CONFIG_PATH)
            .map(PathBuf::from)
            .unwrap_or_else(|_| crate::config::get_base_dir().join("config.toml"));

        if let Some(parent) = config_path.parent() {
            fs::create_dir_all(parent)?;
        }

        let toml_str = toml::to_string_pretty(self)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        fs::write(&config_path, toml_str)
    }

    #[cfg(target_arch = "wasm32")]
    /// Stub implementation for saving configuration in Wasm environments.
    pub fn save(&self) -> Result<(), std::io::Error> {
        Ok(())
    }
}

/// Returns `true` if compile-time simulation mode is enabled (`cfg!(feature = "simulation")`).
///
/// Mathematically guarantees that dev-mode mocks cannot be activated in release builds.
pub fn is_dev_mode() -> bool {
    cfg!(feature = "simulation")
}

/// Returns the path to the directory where local zone JSON files are stored (`{base_dir}/zones`).
pub fn get_zones_dir() -> PathBuf {
    get_base_dir().join("zones")
}

/// Returns the platform-appropriate base directory for Kinetic data files.
///
/// Automatically namespaced by `{TLD}-{NETWORK_ID}` (e.g. `~/.local/share/kinetic/`)
/// to ensure multiple network instances or forks coexist without disk collisions.
/// Overrideable with the `KINETIC_DATA_DIR` environment variable.
pub fn get_base_dir() -> PathBuf {
    if let Ok(path) = std::env::var(crate::constants::ENV_DATA_DIR) {
        return PathBuf::from(path);
    }

    let network_dir = crate::constants::NETWORK_ID;

    #[cfg(not(target_arch = "wasm32"))]
    {
        dirs::data_local_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(network_dir)
    }

    #[cfg(target_arch = "wasm32")]
    {
        PathBuf::from(format!("/{}", network_dir))
    }
}

/// Returns the path to the directory where the scoped CLI API token files are stored (`{base_dir}/tokens/`).
pub fn get_api_tokens_dir() -> PathBuf {
    get_base_dir().join("tokens")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = KineticConfig::default();
        assert_eq!(config.daemon.api_port, ports::API_DAEMON);
        assert_eq!(config.network.daemon_port, ports::P2P_DAEMON);
        assert!(config.network.enable_mdns);
        assert!(config.daemon.enable_dns);
    }

    #[test]
    fn test_bundled_network_json_sync() {
        let root_json_path = PathBuf::from("../network.json");
        let bundled_json_path = PathBuf::from("default_network.json");

        if root_json_path.exists() && bundled_json_path.exists() {
            let root_content = fs::read_to_string(&root_json_path).expect("Failed to read root network.json");
            let bundled_content = fs::read_to_string(&bundled_json_path).expect("Failed to read bundled default_network.json");
            
            assert_eq!(
                root_content, bundled_content,
                "The bundled default_network.json in kinetic-core must perfectly match the root network.json!"
            );
        }
    }
}