Skip to main content

zebra_network/
config.rs

1//! Configuration for Zebra's network communication.
2
3use std::{
4    collections::HashSet,
5    io::{self, ErrorKind},
6    net::{IpAddr, SocketAddr},
7    sync::Arc,
8    time::Duration,
9};
10
11use indexmap::IndexSet;
12use serde::{de, Deserialize, Deserializer};
13use tokio::fs;
14
15use tracing::Span;
16use zebra_chain::{
17    common::atomic_write,
18    parameters::{
19        constants::magics,
20        subsidy::TESTNET_INITIAL_NSM_VALUE_BALANCE,
21        testnet::{
22            self, ConfiguredActivationHeights, ConfiguredCheckpoints, ConfiguredFundingStreams,
23            ConfiguredLockboxDisbursement, RegtestParameters,
24        },
25        Magic, Network, NetworkKind,
26    },
27    work::difficulty::U256,
28};
29
30use crate::{
31    constants::{
32        DEFAULT_CRAWL_NEW_PEER_INTERVAL, DEFAULT_MAX_CONNS_PER_IP,
33        DEFAULT_PEERSET_INITIAL_TARGET_SIZE, DNS_LOOKUP_TIMEOUT, INBOUND_PEER_LIMIT_MULTIPLIER,
34        MAX_PEER_DISK_CACHE_SIZE, OUTBOUND_PEER_LIMIT_MULTIPLIER,
35    },
36    protocol::external::{canonical_peer_addr, canonical_socket_addr},
37    BoxError, PeerSocketAddr,
38};
39
40mod cache_dir;
41
42#[cfg(test)]
43mod tests;
44
45pub use cache_dir::CacheDir;
46
47/// The number of times Zebra will retry each initial peer's DNS resolution,
48/// before checking if any other initial peers have returned addresses.
49///
50/// After doing this number of retries of a failed single peer, Zebra will
51/// check if it has enough peer addresses from other seed peers. If it has
52/// enough addresses, it won't retry this peer again.
53///
54/// If the number of retries is `0`, other peers are checked after every successful
55/// or failed DNS attempt.
56const MAX_SINGLE_SEED_PEER_DNS_RETRIES: usize = 0;
57
58/// Public DNS seed endpoints, also used to isolate configured networks.
59const DEFAULT_MAINNET_PEERS: [&str; 5] = [
60    "dnsseed.str4d.xyz:8233",
61    "dnsseed.z.cash:8233",
62    "mainnet.seeder.shieldedinfra.net:8233",
63    "mainnet.seeder.zfnd.org:8233",
64    "seeder.zec.rocks:8233",
65];
66const DEFAULT_TESTNET_PEERS: [&str; 3] = [
67    "dnsseed.testnet.z.cash:18233",
68    "seeder.testnet.zec.rocks:18233",
69    "testnet.seeder.zfnd.org:18233",
70];
71
72/// Configuration for networking code.
73#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
74#[serde(deny_unknown_fields, default, into = "DConfig")]
75pub struct Config {
76    /// The address on which this node should listen for connections.
77    ///
78    /// Can be `address:port` or just `address`. If there is no configured
79    /// port, Zebra will use the default port for the configured `network`.
80    ///
81    /// `address` can be an IP address or a DNS name. DNS names are
82    /// only resolved once, when Zebra starts up.
83    ///
84    /// By default, Zebra listens on `[::]` (all IPv6 and IPv4 addresses).
85    /// This enables dual-stack support, accepting both IPv4 and IPv6 connections.
86    ///
87    /// If a specific listener address is configured, Zebra will advertise
88    /// it to other nodes. But by default, Zebra uses an unspecified address
89    /// ("\[::\]:port"), which is not advertised to other nodes.
90    ///
91    /// Zebra does not currently support:
92    /// - [Advertising a different external IP address #1890](https://github.com/ZcashFoundation/zebra/issues/1890), or
93    /// - [Auto-discovering its own external IP address #1893](https://github.com/ZcashFoundation/zebra/issues/1893).
94    ///
95    /// However, other Zebra instances compensate for unspecified or incorrect
96    /// listener addresses by adding the external IP addresses of peers to
97    /// their address books.
98    pub listen_addr: SocketAddr,
99
100    /// The external address of this node if any.
101    ///
102    /// Zebra bind to `listen_addr` but this can be an internal address if the node
103    /// is behind a firewall, load balancer or NAT. This field can be used to
104    /// advertise a different address to peers making it possible to receive inbound
105    /// connections and contribute to the P2P network from behind a firewall, load balancer, or NAT.
106    pub external_addr: Option<SocketAddr>,
107
108    /// The network to connect to.
109    pub network: Network,
110
111    /// A list of initial peers for the peerset when operating on
112    /// mainnet.
113    pub initial_mainnet_peers: IndexSet<String>,
114
115    /// A list of initial peers for the peerset when operating on
116    /// testnet.
117    pub initial_testnet_peers: IndexSet<String>,
118
119    /// An optional root directory for storing cached peer address data.
120    ///
121    /// # Configuration
122    ///
123    /// Set to:
124    /// - `true` to read and write peer addresses to disk using the default cache path,
125    /// - `false` to disable reading and writing peer addresses to disk,
126    /// - `'/custom/cache/directory'` to read and write peer addresses to a custom directory.
127    ///
128    /// By default, all Zebra instances run by the same user will share a single peer cache.
129    /// If you use a custom cache path, you might also want to change `state.cache_dir`.
130    ///
131    /// # Functionality
132    ///
133    /// The peer cache is a list of the addresses of some recently useful peers.
134    ///
135    /// For privacy reasons, the cache does *not* include any other information about peers,
136    /// such as when they were connected to the node.
137    ///
138    /// Deleting or modifying the peer cache can impact your node's:
139    /// - reliability: if DNS or the Zcash DNS seeders are unavailable or broken
140    /// - security: if DNS is compromised with malicious peers
141    ///
142    /// If you delete it, Zebra will replace it with a fresh set of peers from the DNS seeders.
143    ///
144    /// # Defaults
145    ///
146    /// The default directory is platform dependent, based on
147    /// [`dirs::cache_dir()`](https://docs.rs/dirs/3.0.1/dirs/fn.cache_dir.html):
148    ///
149    /// |Platform | Value                                           | Example                              |
150    /// | ------- | ----------------------------------------------- | ------------------------------------ |
151    /// | Linux   | `$XDG_CACHE_HOME/zebra` or `$HOME/.cache/zebra` | `/home/alice/.cache/zebra`           |
152    /// | macOS   | `$HOME/Library/Caches/zebra`                    | `/Users/Alice/Library/Caches/zebra`  |
153    /// | Windows | `{FOLDERID_LocalAppData}\zebra`                 | `C:\Users\Alice\AppData\Local\zebra` |
154    /// | Other   | `std::env::current_dir()/cache/zebra`           | `/cache/zebra`                       |
155    ///
156    /// # Security
157    ///
158    /// If you are running Zebra with elevated permissions ("root"), create the
159    /// directory for this file before running Zebra, and make sure the Zebra user
160    /// account has exclusive access to that directory, and other users can't modify
161    /// its parent directories.
162    ///
163    /// # Implementation Details
164    ///
165    /// Each network has a separate peer list, which is updated regularly from the current
166    /// address book. These lists are stored in `network/mainnet.peers` and
167    /// `network/testnet.peers` files, underneath the `cache_dir` path.
168    ///
169    /// Previous peer lists are automatically loaded at startup, and used to populate the
170    /// initial peer set and address book.
171    pub cache_dir: CacheDir,
172
173    /// The initial target size for the peer set.
174    ///
175    /// Also used to limit the number of inbound and outbound connections made by Zebra,
176    /// and the size of the cached peer list.
177    ///
178    /// If you have a slow network connection, and Zebra is having trouble
179    /// syncing, try reducing the peer set size. You can also reduce the peer
180    /// set size to reduce Zebra's bandwidth usage.
181    pub peerset_initial_target_size: usize,
182
183    /// How frequently we attempt to crawl the network to discover new peer
184    /// addresses.
185    ///
186    /// Zebra asks its connected peers for more peer addresses:
187    /// - regularly, every time `crawl_new_peer_interval` elapses, and
188    /// - if the peer set is busy, and there aren't any peer addresses for the
189    ///   next connection attempt.
190    #[serde(with = "humantime_serde")]
191    pub crawl_new_peer_interval: Duration,
192
193    /// The maximum number of peer connections Zebra will keep for a given IP address
194    /// before it drops any additional peer connections with that IP.
195    ///
196    /// IPv6 connections are grouped by `/64` prefix, so the limit applies per
197    /// `/64` subnet rather than per individual address. IPv4 connections are
198    /// limited per address.
199    ///
200    /// The default and minimum value are 1.
201    ///
202    /// # Security
203    ///
204    /// Increasing this config above 1 reduces Zebra's network security.
205    ///
206    /// If this config is greater than 1, Zebra can initiate multiple outbound handshakes to the same
207    /// IP address.
208    ///
209    /// Inbound connection attempts sharing a connection limit key are also rate-limited
210    /// by this config within a short time window.
211    ///
212    /// If Zebra makes multiple inbound or outbound connections to the same IP, they will be dropped
213    /// after the handshake, but before adding them to the peer set. The total numbers of inbound and
214    /// outbound connections are also limited to a multiple of `peerset_initial_target_size`.
215    pub max_connections_per_ip: usize,
216}
217
218impl Config {
219    /// The maximum number of outbound connections that Zebra will open at the same time.
220    /// When this limit is reached, Zebra stops opening outbound connections.
221    ///
222    /// # Security
223    ///
224    /// See the note at [`INBOUND_PEER_LIMIT_MULTIPLIER`].
225    ///
226    /// # Performance
227    ///
228    /// Zebra's peer set should be limited to a reasonable size,
229    /// to avoid queueing too many in-flight block downloads.
230    /// A large queue of in-flight block downloads can choke a
231    /// constrained local network connection.
232    ///
233    /// We assume that Zebra nodes have at least 10 Mbps bandwidth.
234    /// Therefore, a maximum-sized block can take up to 2 seconds to
235    /// download. So the initial outbound peer set adds up to 100 seconds worth
236    /// of blocks to the queue. If Zebra has reached its outbound peer limit,
237    /// that adds an extra 200 seconds of queued blocks.
238    ///
239    /// But the peer set for slow nodes is typically much smaller, due to
240    /// the handshake RTT timeout. And Zebra responds to inbound request
241    /// overloads by dropping peer connections.
242    pub fn peerset_outbound_connection_limit(&self) -> usize {
243        self.peerset_initial_target_size * OUTBOUND_PEER_LIMIT_MULTIPLIER
244    }
245
246    /// The maximum number of inbound connections that Zebra will accept at the same time.
247    /// When this limit is reached, Zebra drops new inbound connections,
248    /// without handshaking on them.
249    ///
250    /// # Security
251    ///
252    /// See the note at [`INBOUND_PEER_LIMIT_MULTIPLIER`].
253    pub fn peerset_inbound_connection_limit(&self) -> usize {
254        self.peerset_initial_target_size * INBOUND_PEER_LIMIT_MULTIPLIER
255    }
256
257    /// The maximum number of inbound and outbound connections that Zebra will have
258    /// at the same time.
259    pub fn peerset_total_connection_limit(&self) -> usize {
260        self.peerset_outbound_connection_limit() + self.peerset_inbound_connection_limit()
261    }
262
263    /// Returns the initial seed peer hostnames for the configured network.
264    pub fn initial_peer_hostnames(&self) -> IndexSet<String> {
265        match &self.network {
266            Network::Mainnet => self.initial_mainnet_peers.clone(),
267            Network::Testnet(params) if params.is_regtest() => self
268                .initial_testnet_peers
269                .iter()
270                .filter(|peer| !is_default_initial_peer(peer))
271                .cloned()
272                .collect(),
273            Network::Testnet(_params) => self.initial_testnet_peers.clone(),
274        }
275    }
276
277    /// Resolve initial seed peer IP addresses, based on the configured network,
278    /// and load cached peers from disk, if available.
279    ///
280    /// # Panics
281    ///
282    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
283    pub async fn initial_peers(&self) -> HashSet<PeerSocketAddr> {
284        // TODO: do DNS and disk in parallel if startup speed becomes important
285        let dns_peers =
286            Config::resolve_peers(&self.initial_peer_hostnames().iter().cloned().collect()).await;
287
288        if self.network.is_regtest() {
289            // Only return local peer addresses and skip loading the peer cache on Regtest.
290            dns_peers
291                .into_iter()
292                .filter(PeerSocketAddr::is_localhost)
293                .collect()
294        } else {
295            // Ignore disk errors because the cache is optional and the method already logs them.
296            let disk_peers = self.load_peer_cache().await.unwrap_or_default();
297
298            dns_peers.into_iter().chain(disk_peers).collect()
299        }
300    }
301
302    /// Concurrently resolves `peers` into zero or more IP addresses, with a
303    /// timeout of a few seconds on each DNS request.
304    ///
305    /// If DNS resolution fails or times out for all peers, continues retrying
306    /// until at least one peer is found.
307    async fn resolve_peers(peers: &HashSet<String>) -> HashSet<PeerSocketAddr> {
308        use futures::stream::StreamExt;
309
310        if peers.is_empty() {
311            warn!(
312                "no initial peers in the network config. \
313                 Hint: you must configure at least one peer IP or DNS seeder to run Zebra, \
314                 give it some previously cached peer IP addresses on disk, \
315                 or make sure Zebra's listener port gets inbound connections."
316            );
317            return HashSet::new();
318        }
319
320        loop {
321            // We retry each peer individually, as well as retrying if there are
322            // no peers in the combined list. DNS failures are correlated, so all
323            // peers can fail DNS, leaving Zebra with a small list of custom IP
324            // address peers. Individual retries avoid this issue.
325            let peer_addresses = peers
326                .iter()
327                .map(|s| Config::resolve_host(s, MAX_SINGLE_SEED_PEER_DNS_RETRIES))
328                .collect::<futures::stream::FuturesUnordered<_>>()
329                .concat()
330                .await;
331
332            if peer_addresses.is_empty() {
333                tracing::info!(
334                    ?peers,
335                    ?peer_addresses,
336                    "empty peer list after DNS resolution, retrying after {} seconds",
337                    DNS_LOOKUP_TIMEOUT.as_secs(),
338                );
339                tokio::time::sleep(DNS_LOOKUP_TIMEOUT).await;
340            } else {
341                return peer_addresses;
342            }
343        }
344    }
345
346    /// Resolves `host` into zero or more IP addresses, retrying up to
347    /// `max_retries` times.
348    ///
349    /// If DNS continues to fail, returns an empty list of addresses.
350    ///
351    /// # Panics
352    ///
353    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
354    async fn resolve_host(host: &str, max_retries: usize) -> HashSet<PeerSocketAddr> {
355        for retries in 0..=max_retries {
356            if let Ok(addresses) = Config::resolve_host_once(host).await {
357                return addresses;
358            }
359
360            if retries < max_retries {
361                tracing::info!(
362                    ?host,
363                    previous_attempts = ?(retries + 1),
364                    "Waiting {DNS_LOOKUP_TIMEOUT:?} to retry seed peer DNS resolution",
365                );
366                tokio::time::sleep(DNS_LOOKUP_TIMEOUT).await;
367            } else {
368                tracing::info!(
369                    ?host,
370                    attempts = ?(retries + 1),
371                    "Seed peer DNS resolution failed, checking for addresses from other seed peers",
372                );
373            }
374        }
375
376        HashSet::new()
377    }
378
379    /// Resolves `host` into zero or more IP addresses.
380    ///
381    /// If `host` is a DNS name, performs DNS resolution with a timeout of a few seconds.
382    /// If DNS resolution fails or times out, returns an error.
383    ///
384    /// # Panics
385    ///
386    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
387    async fn resolve_host_once(host: &str) -> Result<HashSet<PeerSocketAddr>, BoxError> {
388        let fut = tokio::net::lookup_host(host);
389        let fut = tokio::time::timeout(DNS_LOOKUP_TIMEOUT, fut);
390
391        match fut.await {
392            Ok(Ok(ip_addrs)) => {
393                let ip_addrs: Vec<PeerSocketAddr> = ip_addrs.map(canonical_peer_addr).collect();
394
395                // This log is needed for user debugging, but it's annoying during tests.
396                #[cfg(not(test))]
397                info!(seed = ?host, remote_ip_count = ?ip_addrs.len(), "resolved seed peer IP addresses");
398                #[cfg(test)]
399                debug!(seed = ?host, remote_ip_count = ?ip_addrs.len(), "resolved seed peer IP addresses");
400
401                for ip in &ip_addrs {
402                    // Count each initial peer, recording the seed config and resolved IP address.
403                    //
404                    // If an IP is returned by multiple seeds,
405                    // each duplicate adds 1 to the initial peer count.
406                    // (But we only make one initial connection attempt to each IP.)
407                    metrics::counter!(
408                        "zcash.net.peers.initial",
409                        "seed" => host.to_string(),
410                        "remote_ip" => ip.to_string()
411                    )
412                    .increment(1);
413                }
414
415                Ok(ip_addrs.into_iter().collect())
416            }
417            Ok(Err(e)) if e.kind() == ErrorKind::InvalidInput => {
418                // TODO: add testnet/mainnet ports, like we do with the listener address
419                panic!(
420                    "Invalid peer IP address in Zebra config: addresses must have ports:\n\
421                     resolving {host:?} returned {e:?}"
422                );
423            }
424            Ok(Err(e)) => {
425                tracing::info!(?host, ?e, "DNS error resolving peer IP addresses");
426                Err(e.into())
427            }
428            Err(e) => {
429                tracing::info!(?host, ?e, "DNS timeout resolving peer IP addresses");
430                Err(e.into())
431            }
432        }
433    }
434
435    /// Returns the addresses in the peer list cache file, if available.
436    pub async fn load_peer_cache(&self) -> io::Result<HashSet<PeerSocketAddr>> {
437        let Some(peer_cache_file) = self.cache_dir.peer_cache_file_path(&self.network) else {
438            return Ok(HashSet::new());
439        };
440
441        let peer_list = match fs::read_to_string(&peer_cache_file).await {
442            Ok(peer_list) => peer_list,
443            Err(peer_list_error) => {
444                // We expect that the cache will be missing for new Zebra installs
445                if peer_list_error.kind() == ErrorKind::NotFound {
446                    return Ok(HashSet::new());
447                } else {
448                    info!(
449                        ?peer_list_error,
450                        "could not load cached peer list, using default seed peers"
451                    );
452                    return Err(peer_list_error);
453                }
454            }
455        };
456
457        // Skip and log addresses that don't parse, and automatically deduplicate using the HashSet.
458        // (These issues shouldn't happen unless users modify the file.)
459        let peer_list: HashSet<PeerSocketAddr> = peer_list
460            .lines()
461            .filter_map(|peer| {
462                peer.parse()
463                    .map_err(|peer_parse_error| {
464                        info!(
465                            ?peer_parse_error,
466                            "invalid peer address in cached peer list, skipping"
467                        );
468                        peer_parse_error
469                    })
470                    .ok()
471            })
472            .collect();
473
474        // This log is needed for user debugging, but it's annoying during tests.
475        #[cfg(not(test))]
476        info!(
477            cached_ip_count = ?peer_list.len(),
478            ?peer_cache_file,
479            "loaded cached peer IP addresses"
480        );
481        #[cfg(test)]
482        debug!(
483            cached_ip_count = ?peer_list.len(),
484            ?peer_cache_file,
485            "loaded cached peer IP addresses"
486        );
487
488        for ip in &peer_list {
489            // Count each initial peer, recording the cache file and loaded IP address.
490            //
491            // If an IP is returned by DNS seeders and the cache,
492            // each duplicate adds 1 to the initial peer count.
493            // (But we only make one initial connection attempt to each IP.)
494            metrics::counter!(
495                "zcash.net.peers.initial",
496                "cache" => peer_cache_file.display().to_string(),
497                "remote_ip" => ip.to_string()
498            )
499            .increment(1);
500        }
501
502        Ok(peer_list)
503    }
504
505    /// Atomically writes a new `peer_list` to the peer list cache file, if configured.
506    /// If the list is empty, keeps the previous cache file.
507    ///
508    /// Also creates the peer cache directory, if it doesn't already exist.
509    ///
510    /// Atomic writes avoid corrupting the cache if Zebra panics or crashes, or if multiple Zebra
511    /// instances try to read and write the same cache file.
512    pub async fn update_peer_cache(&self, peer_list: HashSet<PeerSocketAddr>) -> io::Result<()> {
513        let Some(peer_cache_file) = self.cache_dir.peer_cache_file_path(&self.network) else {
514            return Ok(());
515        };
516
517        if peer_list.is_empty() {
518            info!(
519                ?peer_cache_file,
520                "cacheable peer list was empty, keeping previous cache"
521            );
522            return Ok(());
523        }
524
525        // Turn IP addresses into strings
526        let mut peer_list: Vec<String> = peer_list
527            .iter()
528            .take(MAX_PEER_DISK_CACHE_SIZE)
529            .map(|redacted_peer| redacted_peer.remove_socket_addr_privacy().to_string())
530            .collect();
531        // # Privacy
532        //
533        // Sort to destroy any peer order, which could leak peer connection times.
534        // (Currently the HashSet argument does this as well.)
535        peer_list.sort();
536        // Make a newline-separated list
537        let peer_data = peer_list.join("\n");
538
539        // Write the peer cache file atomically so the cache is not corrupted if Zebra shuts down
540        // or crashes.
541        let span = Span::current();
542        let write_result = tokio::task::spawn_blocking(move || {
543            span.in_scope(move || atomic_write(peer_cache_file, peer_data.as_bytes()))
544        })
545        .await
546        .expect("could not write the peer cache file")?;
547
548        match write_result {
549            Ok(peer_cache_file) => {
550                info!(
551                    cached_ip_count = ?peer_list.len(),
552                    ?peer_cache_file,
553                    "updated cached peer IP addresses"
554                );
555
556                for ip in &peer_list {
557                    metrics::counter!(
558                        "zcash.net.peers.cache",
559                        "cache" => peer_cache_file.display().to_string(),
560                        "remote_ip" => ip.to_string()
561                    )
562                    .increment(1);
563                }
564
565                Ok(())
566            }
567            Err(error) => Err(error.error),
568        }
569    }
570}
571
572impl Default for Config {
573    fn default() -> Config {
574        let mainnet_peers = DEFAULT_MAINNET_PEERS
575            .into_iter()
576            .map(String::from)
577            .collect();
578        let testnet_peers = DEFAULT_TESTNET_PEERS
579            .into_iter()
580            .map(String::from)
581            .collect();
582
583        Config {
584            listen_addr: "[::]:8233"
585                .parse()
586                .expect("Hardcoded address should be parseable"),
587            external_addr: None,
588            network: Network::Mainnet,
589            initial_mainnet_peers: mainnet_peers,
590            initial_testnet_peers: testnet_peers,
591            cache_dir: CacheDir::default(),
592            crawl_new_peer_interval: DEFAULT_CRAWL_NEW_PEER_INTERVAL,
593
594            // # Security
595            //
596            // The default peerset target size should be large enough to ensure
597            // nodes have a reliable set of peers.
598            //
599            // But Zebra should only make a small number of initial outbound connections,
600            // so that idle peers don't use too many connection slots.
601            peerset_initial_target_size: DEFAULT_PEERSET_INITIAL_TARGET_SIZE,
602            max_connections_per_ip: DEFAULT_MAX_CONNS_PER_IP,
603        }
604    }
605}
606
607#[derive(Serialize, Deserialize)]
608#[serde(deny_unknown_fields)]
609struct DTestnetParameters {
610    network_name: Option<String>,
611    network_magic: Option<[u8; 4]>,
612    slow_start_interval: Option<u32>,
613    target_difficulty_limit: Option<String>,
614    disable_pow: Option<bool>,
615    genesis_hash: Option<String>,
616    activation_heights: Option<ConfiguredActivationHeights>,
617    /// Override for the first NSM reissuance height; omission uses ZIP 237's crossover.
618    nsm_reissuance_height: Option<zebra_chain::block::Height>,
619    pre_nu6_funding_streams: Option<ConfiguredFundingStreams>,
620    post_nu6_funding_streams: Option<ConfiguredFundingStreams>,
621    /// Omission retains the default streams; an explicitly empty list disables them.
622    /// An empty list cannot be combined with either legacy funding stream field.
623    funding_streams: Option<Vec<ConfiguredFundingStreams>>,
624    /// Must yield a supported first halving height and a schedule within the monetary cap.
625    /// Configured reissuance needs a positive coefficient; funding streams need a nonzero
626    /// address-change interval.
627    pre_blossom_halving_interval: Option<u32>,
628    lockbox_disbursements: Option<Vec<ConfiguredLockboxDisbursement>>,
629    #[serde(default)]
630    checkpoints: ConfiguredCheckpoints,
631    /// If `true`, automatically repeats configured funding stream addresses to fill
632    /// all required periods.
633    extend_funding_stream_addresses_as_required: Option<bool>,
634    /// Height at which the soft fork that temporarily disables Orchard actions activates.
635    ///
636    /// If unset, the default activation height for the network is used; the soft fork
637    /// cannot be disabled via configuration.
638    temporary_orchard_disabling_soft_fork_height: Option<u32>,
639    /// Regtest only: whether to allow coinbase spends to have transparent outputs.
640    should_allow_unshielded_coinbase_spends: Option<bool>,
641
642    /// Regtest and configured Testnets only: the NSM value balance seeded at NU7 activation
643    /// under the halving-preserving issuance ZIP.
644    initial_nsm_value_balance:
645        Option<zebra_chain::amount::Amount<zebra_chain::amount::NonNegative>>,
646}
647
648/// Network configuration used during deserialization.
649#[derive(Serialize, Deserialize)]
650#[serde(untagged)]
651enum DNetwork {
652    DefaultForKind(NetworkKind),
653    ConfiguredRegtest {
654        params: Box<DTestnetParameters>,
655
656        #[serde(default, skip_serializing)]
657        regtest: Option<bool>,
658    },
659    ConfiguredTestnet(Box<DTestnetParameters>),
660}
661
662impl Default for DNetwork {
663    fn default() -> Self {
664        DNetwork::DefaultForKind(NetworkKind::Mainnet)
665    }
666}
667
668#[derive(Serialize, Deserialize)]
669#[serde(deny_unknown_fields, default)]
670struct DConfig {
671    listen_addr: String,
672    external_addr: Option<String>,
673    network: DNetwork,
674
675    /// Legacy testnet parameters, kept for backwards compatibility.
676    #[serde(default, skip_serializing_if = "Option::is_none")]
677    testnet_parameters: Option<DTestnetParameters>,
678
679    initial_mainnet_peers: IndexSet<String>,
680    initial_testnet_peers: IndexSet<String>,
681    cache_dir: CacheDir,
682    peerset_initial_target_size: usize,
683    #[serde(alias = "new_peer_interval", with = "humantime_serde")]
684    crawl_new_peer_interval: Duration,
685    max_connections_per_ip: Option<usize>,
686}
687
688impl Default for DConfig {
689    fn default() -> Self {
690        let config = Config::default();
691        Self {
692            listen_addr: "[::]".to_string(),
693            external_addr: None,
694            network: Default::default(),
695            testnet_parameters: None,
696            initial_mainnet_peers: config.initial_mainnet_peers,
697            initial_testnet_peers: config.initial_testnet_peers,
698            cache_dir: config.cache_dir,
699            peerset_initial_target_size: config.peerset_initial_target_size,
700            crawl_new_peer_interval: config.crawl_new_peer_interval,
701            max_connections_per_ip: Some(config.max_connections_per_ip),
702        }
703    }
704}
705
706impl From<Arc<testnet::Parameters>> for DTestnetParameters {
707    fn from(params: Arc<testnet::Parameters>) -> Self {
708        Self {
709            network_name: Some(params.network_name().to_string()),
710            network_magic: Some(params.network_magic().0),
711            slow_start_interval: Some(params.slow_start_interval().0),
712            target_difficulty_limit: Some(params.target_difficulty_limit().to_string()),
713            disable_pow: Some(params.disable_pow()),
714            genesis_hash: Some(params.genesis_hash().to_string()),
715            activation_heights: Some(params.activation_heights().into()),
716            nsm_reissuance_height: params.configured_nsm_reissuance_height(),
717            pre_nu6_funding_streams: None,
718            post_nu6_funding_streams: None,
719            initial_nsm_value_balance: params.configured_initial_nsm_value_balance(),
720            funding_streams: Some(params.funding_streams().iter().map(Into::into).collect()),
721            pre_blossom_halving_interval: Some(
722                params
723                    .pre_blossom_halving_interval()
724                    .try_into()
725                    .expect("should convert"),
726            ),
727            lockbox_disbursements: Some(
728                params
729                    .lockbox_disbursements()
730                    .into_iter()
731                    .map(Into::into)
732                    .collect(),
733            ),
734            checkpoints: if params.checkpoints() == testnet::Parameters::default().checkpoints() {
735                ConfiguredCheckpoints::Default(true)
736            } else {
737                params.checkpoints().into()
738            },
739            extend_funding_stream_addresses_as_required: None,
740            temporary_orchard_disabling_soft_fork_height: params
741                .temporary_orchard_disabling_soft_fork_height()
742                .map(|height| height.0),
743            should_allow_unshielded_coinbase_spends: params
744                .is_regtest()
745                .then(|| params.should_allow_unshielded_coinbase_spends()),
746        }
747    }
748}
749
750impl From<Config> for DConfig {
751    fn from(
752        Config {
753            listen_addr,
754            external_addr,
755            network,
756            initial_mainnet_peers,
757            initial_testnet_peers,
758            cache_dir,
759            peerset_initial_target_size,
760            crawl_new_peer_interval,
761            max_connections_per_ip,
762        }: Config,
763    ) -> Self {
764        let dnetwork = match network.kind() {
765            NetworkKind::Testnet => match network
766                .parameters()
767                .filter(|params| !params.is_default_testnet())
768                .map(Into::into)
769            {
770                Some(params) => DNetwork::ConfiguredTestnet(Box::new(params)),
771                None => DNetwork::DefaultForKind(NetworkKind::Testnet),
772            },
773
774            NetworkKind::Regtest => match network.parameters().map(Into::into) {
775                Some(params) => DNetwork::ConfiguredRegtest {
776                    params: Box::new(params),
777                    regtest: Some(true),
778                },
779                None => DNetwork::DefaultForKind(NetworkKind::Regtest),
780            },
781
782            other_kind => DNetwork::DefaultForKind(other_kind),
783        };
784
785        DConfig {
786            listen_addr: listen_addr.to_string(),
787            external_addr: external_addr.map(|addr| addr.to_string()),
788            network: dnetwork,
789            testnet_parameters: None,
790            initial_mainnet_peers,
791            initial_testnet_peers,
792            cache_dir,
793            peerset_initial_target_size,
794            crawl_new_peer_interval,
795            max_connections_per_ip: Some(max_connections_per_ip),
796        }
797    }
798}
799
800impl<'de> Deserialize<'de> for Config {
801    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
802    where
803        D: Deserializer<'de>,
804    {
805        let DConfig {
806            listen_addr,
807            external_addr,
808            network: dnetwork,
809            testnet_parameters,
810            initial_mainnet_peers,
811            initial_testnet_peers,
812            cache_dir,
813            peerset_initial_target_size,
814            crawl_new_peer_interval,
815            max_connections_per_ip,
816        } = DConfig::deserialize(deserializer)?;
817
818        let network = match (dnetwork, testnet_parameters) {
819            (DNetwork::ConfiguredTestnet(params), _) => {
820                build_configured_testnet::<D>(*params, &initial_testnet_peers)?
821            }
822            (DNetwork::ConfiguredRegtest { params, .. }, _) => testnet::Parameters::new_regtest(
823                build_regtest_params(*params).map_err(de::Error::custom)?,
824            )
825            .map(Network::new_configured_testnet)
826            .map_err(de::Error::custom)?,
827            (DNetwork::DefaultForKind(NetworkKind::Mainnet), _) => Network::Mainnet,
828            (DNetwork::DefaultForKind(NetworkKind::Testnet), Some(params)) => {
829                build_configured_testnet::<D>(params, &initial_testnet_peers)?
830            }
831            (DNetwork::DefaultForKind(NetworkKind::Testnet), None) => {
832                Network::new_default_testnet()
833            }
834            (DNetwork::DefaultForKind(NetworkKind::Regtest), Some(params)) => {
835                testnet::Parameters::new_regtest(
836                    build_regtest_params(params).map_err(de::Error::custom)?,
837                )
838                .map(Network::new_configured_testnet)
839                .map_err(de::Error::custom)?
840            }
841            (DNetwork::DefaultForKind(NetworkKind::Regtest), None) => {
842                Network::new_regtest(Default::default())
843            }
844        };
845
846        let listen_addr = match listen_addr.parse::<SocketAddr>().or_else(|_| format!("{listen_addr}:{}", network.default_port()).parse()) {
847            Ok(socket) => Ok(socket),
848            Err(_) => match listen_addr.parse::<IpAddr>() {
849                Ok(ip) => Ok(SocketAddr::new(ip, network.default_port())),
850                Err(err) => Err(de::Error::custom(format!(
851                    "{err}; Hint: addresses can be a IPv4, IPv6 (with brackets), or a DNS name, the port is optional"
852                ))),
853            },
854        }?;
855
856        let external_socket_addr = if let Some(address) = &external_addr {
857            match address.parse::<SocketAddr>().or_else(|_| format!("{address}:{}", network.default_port()).parse()) {
858                Ok(socket) => Ok(Some(socket)),
859                Err(_) => match address.parse::<IpAddr>() {
860                    Ok(ip) => Ok(Some(SocketAddr::new(ip, network.default_port()))),
861                    Err(err) => Err(de::Error::custom(format!(
862                        "{err}; Hint: addresses can be a IPv4, IPv6 (with brackets), or a DNS name, the port is optional"
863                    ))),
864                },
865            }?
866        } else {
867            None
868        };
869
870        let [max_connections_per_ip, peerset_initial_target_size] = [
871            ("max_connections_per_ip", max_connections_per_ip, DEFAULT_MAX_CONNS_PER_IP),
872            // If we want Zebra to operate with no network,
873            // we should implement a `zebrad` command that doesn't use `zebra-network`.
874            ("peerset_initial_target_size", Some(peerset_initial_target_size), DEFAULT_PEERSET_INITIAL_TARGET_SIZE)
875        ].map(|(field_name, non_zero_config_field, default_config_value)| {
876            if non_zero_config_field == Some(0) {
877                warn!(
878                    ?field_name,
879                    ?non_zero_config_field,
880                    "{field_name} should be greater than 0, using default value of {default_config_value} instead"
881                );
882            }
883
884            non_zero_config_field.filter(|config_value| config_value > &0).unwrap_or(default_config_value)
885        });
886
887        Ok(Config {
888            listen_addr: canonical_socket_addr(listen_addr),
889            external_addr: external_socket_addr,
890            network,
891            initial_mainnet_peers,
892            initial_testnet_peers,
893            cache_dir,
894            peerset_initial_target_size,
895            crawl_new_peer_interval,
896            max_connections_per_ip,
897        })
898    }
899}
900
901/// Returns true for a public DNS seed hostname, regardless of case, trailing dot, or port.
902fn is_default_initial_peer(peer: &str) -> bool {
903    fn hostname(peer: &str) -> &str {
904        peer.rsplit_once(':').map_or(peer, |(host, _)| host)
905    }
906    let peer_host = hostname(peer).trim_end_matches('.');
907    DEFAULT_MAINNET_PEERS
908        .iter()
909        .chain(&DEFAULT_TESTNET_PEERS)
910        .any(|default| peer_host.eq_ignore_ascii_case(hostname(default)))
911}
912
913fn build_configured_testnet<'de, D>(
914    params: DTestnetParameters,
915    initial_testnet_peers: &IndexSet<String>,
916) -> Result<Network, D::Error>
917where
918    D: Deserializer<'de>,
919{
920    let DTestnetParameters {
921        network_name,
922        network_magic,
923        slow_start_interval,
924        target_difficulty_limit,
925        disable_pow,
926        genesis_hash,
927        activation_heights,
928        nsm_reissuance_height,
929        pre_nu6_funding_streams,
930        post_nu6_funding_streams,
931        funding_streams,
932        pre_blossom_halving_interval,
933        lockbox_disbursements,
934        checkpoints,
935        extend_funding_stream_addresses_as_required,
936        temporary_orchard_disabling_soft_fork_height,
937        should_allow_unshielded_coinbase_spends,
938        initial_nsm_value_balance,
939    } = params;
940
941    // This is a Regtest-only consensus knob, so reject it rather than silently ignoring it.
942    if should_allow_unshielded_coinbase_spends.is_some() {
943        return Err(de::Error::custom(
944            "should_allow_unshielded_coinbase_spends is only supported on Regtest",
945        ));
946    }
947
948    let mut params_builder = testnet::Parameters::build();
949
950    // A redundant public seed must not turn public Testnet into a configured network.
951    if let Some(initial_nsm_value_balance) = initial_nsm_value_balance.filter(|seed| {
952        network_magic.map(Magic).unwrap_or(magics::TESTNET) != magics::TESTNET
953            || u64::from(*seed) != TESTNET_INITIAL_NSM_VALUE_BALANCE
954    }) {
955        params_builder = params_builder.with_initial_nsm_value_balance(initial_nsm_value_balance);
956    }
957
958    if let Some(network_name) = network_name.clone() {
959        params_builder = params_builder
960            .with_network_name(network_name)
961            .map_err(de::Error::custom)?
962    }
963
964    if let Some(network_magic) = network_magic {
965        params_builder = params_builder
966            .with_network_magic(Magic(network_magic))
967            .map_err(de::Error::custom)?;
968    }
969
970    if let Some(genesis_hash) = genesis_hash {
971        params_builder = params_builder
972            .with_genesis_hash(genesis_hash)
973            .map_err(de::Error::custom)?;
974    }
975
976    if let Some(slow_start_interval) = slow_start_interval {
977        params_builder = params_builder
978            .with_slow_start_interval(slow_start_interval.try_into().map_err(de::Error::custom)?);
979    }
980
981    if let Some(target_difficulty_limit) = target_difficulty_limit.clone() {
982        params_builder = params_builder
983            .with_target_difficulty_limit(
984                target_difficulty_limit
985                    .parse::<U256>()
986                    .map_err(de::Error::custom)?,
987            )
988            .map_err(de::Error::custom)?;
989    }
990
991    if let Some(disable_pow) = disable_pow {
992        params_builder = params_builder.with_disable_pow(disable_pow);
993    }
994
995    // Retain default Testnet activation heights unless there's an empty [testnet_parameters.activation_heights] section.
996    if let Some(activation_heights) = activation_heights {
997        params_builder = params_builder
998            .with_activation_heights(activation_heights)
999            .map_err(de::Error::custom)?
1000    }
1001
1002    if let Some(height) = nsm_reissuance_height {
1003        params_builder = params_builder.with_nsm_reissuance_height(height);
1004    }
1005
1006    if let Some(halving_interval) = pre_blossom_halving_interval {
1007        params_builder = params_builder
1008            .with_halving_interval(halving_interval.into())
1009            .map_err(de::Error::custom)?
1010    }
1011
1012    // Set configured funding streams after setting any parameters that affect the funding stream address period.
1013    if let Some(funding_streams) = merge_funding_streams(
1014        funding_streams,
1015        pre_nu6_funding_streams,
1016        post_nu6_funding_streams,
1017    )
1018    .map_err(de::Error::custom)?
1019    {
1020        params_builder = params_builder.with_funding_streams(funding_streams);
1021    }
1022
1023    if let Some(lockbox_disbursements) = lockbox_disbursements {
1024        params_builder = params_builder.with_lockbox_disbursements(lockbox_disbursements);
1025    }
1026
1027    params_builder = params_builder
1028        .with_checkpoints(checkpoints)
1029        .map_err(de::Error::custom)?;
1030
1031    if let Some(true) = extend_funding_stream_addresses_as_required {
1032        params_builder = params_builder
1033            .extend_funding_streams()
1034            .map_err(de::Error::custom)?;
1035    }
1036
1037    // Retain the default soft-fork activation height unless one is configured.
1038    if let Some(height) = temporary_orchard_disabling_soft_fork_height {
1039        params_builder = params_builder.with_temporary_orchard_disabling_soft_fork_height(
1040            height.try_into().map_err(de::Error::custom)?,
1041        );
1042    }
1043
1044    // Preserve the canonical public Testnet representation when no parameters changed.
1045    if network_name.is_none() && params_builder == testnet::Parameters::build() {
1046        return Ok(Network::new_default_testnet());
1047    }
1048
1049    if !params_builder.is_compatible_with_default_parameters() {
1050        // Keep the diagnostic for explicitly configured public seeds, even with isolated magic.
1051        if initial_testnet_peers
1052            .iter()
1053            .any(|peer| is_default_initial_peer(peer))
1054        {
1055            return Err(de::Error::custom(
1056                "cannot use default initial peers with incompatible testnet; \
1057                 for public Testnet, remove explicit consensus overrides from \
1058                 [network.testnet_parameters] to use the updated public defaults, including NU7; \
1059                 for a custom network, configure its own initial_testnet_peers and \
1060                 network_magic distinct from public Testnet",
1061            ));
1062        }
1063
1064        // Peer names cannot enforce isolation: aliases, cached peers, and inbound connections
1065        // can all reach public Testnet. Incompatible consensus rules require distinct wire magic.
1066        if network_magic.map(Magic).unwrap_or(magics::TESTNET) == magics::TESTNET {
1067            return Err(de::Error::custom(
1068                "incompatible testnet parameters require network_magic distinct from public Testnet; \
1069                 for public Testnet, remove explicit consensus overrides from \
1070                 [network.testnet_parameters] (including activation_heights and funding_streams) \
1071                 to use the updated public defaults, including NU7; for a custom network, configure its own \
1072                 network_magic and initial_testnet_peers",
1073            ));
1074        }
1075    }
1076
1077    params_builder.to_network().map_err(de::Error::custom)
1078}
1079
1080fn build_regtest_params(params: DTestnetParameters) -> Result<RegtestParameters, &'static str> {
1081    let DTestnetParameters {
1082        activation_heights,
1083        nsm_reissuance_height,
1084        pre_nu6_funding_streams,
1085        post_nu6_funding_streams,
1086        funding_streams,
1087        lockbox_disbursements,
1088        checkpoints,
1089        extend_funding_stream_addresses_as_required,
1090        should_allow_unshielded_coinbase_spends,
1091        initial_nsm_value_balance,
1092        ..
1093    } = params;
1094
1095    Ok(RegtestParameters {
1096        activation_heights: activation_heights.unwrap_or_default(),
1097        nsm_reissuance_height,
1098        funding_streams: merge_funding_streams(
1099            funding_streams,
1100            pre_nu6_funding_streams,
1101            post_nu6_funding_streams,
1102        )?,
1103        lockbox_disbursements,
1104        checkpoints: Some(checkpoints),
1105        extend_funding_stream_addresses_as_required,
1106        should_allow_unshielded_coinbase_spends,
1107        initial_nsm_value_balance,
1108    })
1109}
1110
1111/// Merge legacy streams before the current list, preserving omission and explicit disable.
1112fn merge_funding_streams(
1113    funding_streams: Option<Vec<ConfiguredFundingStreams>>,
1114    pre_nu6_funding_streams: Option<ConfiguredFundingStreams>,
1115    post_nu6_funding_streams: Option<ConfiguredFundingStreams>,
1116) -> Result<Option<Vec<ConfiguredFundingStreams>>, &'static str> {
1117    let has_legacy_streams =
1118        pre_nu6_funding_streams.is_some() || post_nu6_funding_streams.is_some();
1119    if funding_streams.as_ref().is_some_and(Vec::is_empty) && has_legacy_streams {
1120        return Err(
1121            "funding_streams = [] cannot be combined with pre_nu6_funding_streams or post_nu6_funding_streams",
1122        );
1123    }
1124
1125    let specified = funding_streams.is_some() || has_legacy_streams;
1126    let mut funding_streams = funding_streams.unwrap_or_default();
1127    if let Some(mut post_nu6) = post_nu6_funding_streams {
1128        // Legacy names select their own defaults, not their position in the merged list.
1129        if post_nu6.height_range.is_none() || post_nu6.recipients.is_none() {
1130            let default_parameters = testnet::Parameters::default();
1131            let defaults = &default_parameters.funding_streams()[1];
1132            post_nu6
1133                .height_range
1134                .get_or_insert_with(|| defaults.height_range().clone());
1135            if post_nu6.recipients.is_none() {
1136                post_nu6.recipients = ConfiguredFundingStreams::from(defaults).recipients;
1137            }
1138        }
1139        funding_streams.insert(0, post_nu6);
1140    }
1141    if let Some(pre_nu6) = pre_nu6_funding_streams {
1142        funding_streams.insert(0, pre_nu6);
1143    }
1144
1145    Ok(specified.then_some(funding_streams))
1146}