Skip to main content

tenzro_network/
config.rs

1//! Network configuration for Tenzro Network
2
3use libp2p::Multiaddr;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Network configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct NetworkConfig {
11    /// Data directory for persistent state (keypair, etc.)
12    pub data_dir: Option<PathBuf>,
13
14    /// Listen addresses (multiaddr format)
15    pub listen_addresses: Vec<Multiaddr>,
16
17    /// Bootstrap nodes to connect to on startup
18    pub boot_nodes: Vec<Multiaddr>,
19
20    /// Maximum number of inbound peers
21    pub max_inbound_peers: u32,
22
23    /// Maximum number of outbound peers
24    pub max_outbound_peers: u32,
25
26    /// Topics to subscribe to on startup
27    pub gossip_topics: Vec<String>,
28
29    /// Enable DHT for peer discovery
30    pub enable_dht: bool,
31
32    /// Enable mDNS for local peer discovery
33    pub enable_mdns: bool,
34
35    /// Connection idle timeout
36    pub connection_idle_timeout: Duration,
37
38    /// Heartbeat interval for gossipsub
39    pub gossip_heartbeat_interval: Duration,
40
41    /// Protocol version
42    pub protocol_version: String,
43
44    /// User agent string
45    pub user_agent: String,
46
47    /// Enable hole punching for NAT traversal
48    pub enable_hole_punching: bool,
49
50    /// Enable relay server
51    pub enable_relay: bool,
52
53    /// Statically-configured external addresses this node advertises to peers
54    /// via Identify. Required on cloud deployments where the host binds to
55    /// `0.0.0.0` and Identify's listen-addr enumeration is hidden (see
56    /// `behaviour.rs::hide_listen_addrs`). Each multiaddr listed here is
57    /// passed to `Swarm::add_external_address` at startup.
58    ///
59    /// For validator-0 in the live GCE testnet this is set to
60    /// `/ip4/<external-ip>/tcp/9000` from the per-VM Terraform output. For
61    /// nodes whose external address must be discovered (community joiners
62    /// behind NAT), leave this empty and rely on AutoNAT v2 + Relay v2 +
63    /// DCUtR to confirm a reachable address dynamically.
64    pub external_addresses: Vec<Multiaddr>,
65}
66
67impl Default for NetworkConfig {
68    fn default() -> Self {
69        Self {
70            data_dir: None,
71            listen_addresses: vec![
72                "/ip4/0.0.0.0/tcp/9000".parse().unwrap(),
73                "/ip4/0.0.0.0/udp/9000/quic-v1".parse().unwrap(),
74            ],
75            boot_nodes: Vec::new(),
76            // A+++ production-grade capacity: matches libp2p ConnectionLimits (200/200 max)
77            max_inbound_peers: 200,
78            max_outbound_peers: 200,
79            gossip_topics: vec![
80                "tenzro/blocks".to_string(),
81                "tenzro/transactions".to_string(),
82                "tenzro/consensus".to_string(),
83            ],
84            enable_dht: true,
85            // SECURITY: mDNS leaks LAN presence — disabled by default for production;
86            // `local()` preset re-enables it explicitly for dev.
87            enable_mdns: false,
88            // 10 minutes. Two reasons it has to be this high on cloud deployments:
89            // (1) rust-libp2p closes any connection that stays idle this long with
90            //     no in-flight substream — and HotStuff-2 has long quiet pairwise
91            //     links during normal operation. A short value (e.g. 120s) causes
92            //     constant close+redial churn and degrades the gossipsub mesh from
93            //     full N-1 to 0-4 peers over an hour, stalling consensus (observed
94            //     on GCE multi-region 2026-05-14).
95            // (2) GCE's VPC fabric (Andromeda) silently evicts idle conntrack
96            //     entries at 10 minutes WITHOUT sending RST. Keeping libp2p's
97            //     idle timeout at or above 600s, combined with kernel TCP keepalive
98            //     set to fire every ~120s (configured at the host layer in
99            //     deploy/terraform/gce_validators/cloud-init.yaml), keeps the
100            //     conntrack entries warm before either side drops the connection.
101            connection_idle_timeout: Duration::from_secs(600),
102            // Ethereum-class 700ms heartbeat for sub-second gossip propagation.
103            gossip_heartbeat_interval: Duration::from_millis(700),
104            protocol_version: "tenzro/1.0.0".to_string(),
105            user_agent: "tenzro-network/0.1.0".to_string(),
106            enable_hole_punching: true,
107            enable_relay: false,
108            external_addresses: Vec::new(),
109        }
110    }
111}
112
113impl NetworkConfig {
114    /// Creates a testnet configuration
115    pub fn testnet() -> Self {
116        Self {
117            boot_nodes: vec![
118                "/dns4/testnet-boot-1.tenzro.network/tcp/9000".parse().unwrap(),
119                "/dns4/testnet-boot-2.tenzro.network/tcp/9000".parse().unwrap(),
120            ],
121            gossip_topics: vec![
122                "tenzro/testnet/blocks/1.0.0".to_string(),
123                "tenzro/testnet/transactions/1.0.0".to_string(),
124                "tenzro/testnet/consensus/1.0.0".to_string(),
125                "tenzro/testnet/attestations/1.0.0".to_string(),
126                "tenzro/testnet/models/1.0.0".to_string(),
127                "tenzro/testnet/inference/1.0.0".to_string(),
128            ],
129            enable_mdns: false,
130            ..Default::default()
131        }
132    }
133
134    /// Creates a mainnet configuration
135    pub fn mainnet() -> Self {
136        Self {
137            boot_nodes: vec![
138                "/dns4/mainnet-boot-1.tenzro.network/tcp/9000".parse().unwrap(),
139                "/dns4/mainnet-boot-2.tenzro.network/tcp/9000".parse().unwrap(),
140                "/dns4/mainnet-boot-3.tenzro.network/tcp/9000".parse().unwrap(),
141            ],
142            gossip_topics: vec![
143                "tenzro/mainnet/blocks/1.0.0".to_string(),
144                "tenzro/mainnet/transactions/1.0.0".to_string(),
145                "tenzro/mainnet/consensus/1.0.0".to_string(),
146                "tenzro/mainnet/attestations/1.0.0".to_string(),
147                "tenzro/mainnet/models/1.0.0".to_string(),
148                "tenzro/mainnet/inference/1.0.0".to_string(),
149            ],
150            enable_relay: false,
151            enable_mdns: false,
152            ..Default::default()
153        }
154    }
155
156    /// Creates a local development configuration
157    pub fn local() -> Self {
158        Self {
159            listen_addresses: vec![
160                "/ip4/127.0.0.1/tcp/0".parse().unwrap(),
161                "/ip4/127.0.0.1/udp/0/quic-v1".parse().unwrap(),
162            ],
163            boot_nodes: Vec::new(),
164            max_inbound_peers: 10,
165            max_outbound_peers: 5,
166            // mDNS only re-enabled for local dev — never for test/main-net.
167            enable_mdns: true,
168            enable_dht: false,
169            ..Default::default()
170        }
171    }
172
173    /// Validates the configuration
174    pub fn validate(&self) -> crate::error::Result<()> {
175        if self.max_inbound_peers == 0 && self.max_outbound_peers == 0 {
176            return Err(crate::error::NetworkError::InvalidConfig("At least one peer connection must be allowed".to_string()));
177        }
178
179        if self.listen_addresses.is_empty() {
180            return Err(crate::error::NetworkError::InvalidConfig("At least one listen address must be specified".to_string()));
181        }
182
183        if self.gossip_topics.is_empty() {
184            return Err(crate::error::NetworkError::InvalidConfig("At least one gossip topic must be specified".to_string()));
185        }
186
187        Ok(())
188    }
189}