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}