Skip to main content

zakura_network/
config.rs

1//! Configuration for Zakura's network communication.
2
3use std::{
4    collections::HashSet,
5    fmt,
6    io::{self, ErrorKind},
7    net::{IpAddr, SocketAddr},
8    path::{Path, PathBuf},
9    str::FromStr,
10    sync::Arc,
11    time::Duration,
12};
13
14use indexmap::IndexSet;
15use iroh::SecretKey;
16use rand::rngs::OsRng;
17use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
18use tokio::fs;
19
20use tracing::Span;
21use zakura_chain::{
22    common::atomic_write,
23    parameters::{
24        testnet::{
25            self, ConfiguredActivationHeights, ConfiguredCheckpoints, ConfiguredFundingStreams,
26            ConfiguredLockboxDisbursement, RegtestParameters,
27        },
28        Magic, Network, NetworkKind,
29    },
30    work::difficulty::U256,
31};
32
33use crate::{
34    constants::{
35        DEFAULT_CRAWL_NEW_PEER_INTERVAL, DEFAULT_MAX_CONNS_PER_IP,
36        DEFAULT_PEERSET_INITIAL_TARGET_SIZE, DNS_LOOKUP_TIMEOUT, INBOUND_PEER_LIMIT_MULTIPLIER,
37        MAX_PEER_DISK_CACHE_SIZE, OUTBOUND_PEER_LIMIT_MULTIPLIER,
38    },
39    protocol::external::{canonical_peer_addr, canonical_socket_addr},
40    zakura::ZakuraConfig,
41    BoxError, PeerSocketAddr,
42};
43
44mod cache_dir;
45
46#[cfg(test)]
47mod tests;
48
49pub use cache_dir::CacheDir;
50
51pub(crate) use cache_dir::{
52    default_network_identity_dir, zakura_node_secret_key_file_path as zakura_secret_key_file_path,
53};
54
55/// A sensitive iroh secret-key override for Zakura P2P node identity.
56#[derive(Clone, Deserialize, Eq, PartialEq)]
57#[serde(transparent)]
58pub struct ZakuraNodeSecretKey(String);
59
60impl ZakuraNodeSecretKey {
61    /// Returns the secret-key override.
62    ///
63    /// Callers should only expose this value to iroh identity construction or
64    /// controlled persistence paths.
65    pub fn expose_secret(&self) -> &str {
66        &self.0
67    }
68}
69
70impl fmt::Debug for ZakuraNodeSecretKey {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.write_str("ZakuraNodeSecretKey([redacted])")
73    }
74}
75
76impl serde::Serialize for ZakuraNodeSecretKey {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79        S: Serializer,
80    {
81        serializer.serialize_str("[redacted]")
82    }
83}
84
85/// An error that can occur while resolving the Zakura node secret key.
86#[derive(Clone, Debug, thiserror::Error)]
87pub enum ZakuraSecretKeyError {
88    /// The configured `zakura_node_secret_key` is not a valid iroh secret key.
89    #[error("configured zakura_node_secret_key is not a valid iroh secret key")]
90    InvalidConfigured,
91}
92
93/// The number of times Zakura will retry each initial peer's DNS resolution,
94/// before checking if any other initial peers have returned addresses.
95///
96/// After doing this number of retries of a failed single peer, Zakura will
97/// check if it has enough peer addresses from other seed peers. If it has
98/// enough addresses, it won't retry this peer again.
99///
100/// If the number of retries is `0`, other peers are checked after every successful
101/// or failed DNS attempt.
102const MAX_SINGLE_SEED_PEER_DNS_RETRIES: usize = 0;
103
104/// The peer-to-peer stack Zakura runs, selected by `network.p2p_stack` in `zakurad.toml`.
105///
106/// [`P2pStack::Default`] is a placeholder for the configured network's binary default; it is
107/// turned into one of the three real stacks by [`P2pStack::resolve`].
108#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
109#[serde(rename_all = "lowercase")]
110pub enum P2pStack {
111    /// Follow the configured network's binary default, which can change between releases.
112    #[default]
113    Default,
114
115    /// The legacy TCP Zcash P2P stack only.
116    Legacy,
117
118    /// The experimental native Zakura P2P v2 stack only.
119    Zakura,
120
121    /// Both stacks: mutually capable peers are upgraded to the experimental Zakura P2P v2
122    /// stack, and the legacy stack stays available for peers that can't upgrade.
123    Dual,
124}
125
126impl P2pStack {
127    /// Resolves [`P2pStack::Default`] to the binary default for `network`, and returns every
128    /// other stack unchanged.
129    ///
130    /// Mainnet defaults to [`P2pStack::Legacy`] until Zakura P2P v2 is proven there. Every other
131    /// network defaults to [`P2pStack::Dual`], so Zakura P2P v2 gets exercised while legacy
132    /// peers stay reachable.
133    pub fn resolve(self, network: &Network) -> P2pStack {
134        match self {
135            P2pStack::Default if matches!(network, Network::Mainnet) => P2pStack::Legacy,
136            P2pStack::Default => P2pStack::Dual,
137            resolved => resolved,
138        }
139    }
140
141    /// Returns `true` if this stack runs the legacy TCP Zcash P2P listener, dialer, and crawler.
142    ///
143    /// [`P2pStack::Default`] runs neither stack: resolve it with [`P2pStack::resolve`] first.
144    fn runs_legacy(self) -> bool {
145        matches!(self, P2pStack::Legacy | P2pStack::Dual)
146    }
147
148    /// Returns `true` if this stack runs the native Zakura P2P v2 endpoint.
149    ///
150    /// [`P2pStack::Default`] runs neither stack: resolve it with [`P2pStack::resolve`] first.
151    fn runs_zakura(self) -> bool {
152        matches!(self, P2pStack::Zakura | P2pStack::Dual)
153    }
154}
155
156/// Configuration for networking code.
157#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
158#[serde(deny_unknown_fields, default, into = "DConfig")]
159pub struct Config {
160    /// The address on which this node should listen for connections.
161    ///
162    /// Can be `address:port` or just `address`. If there is no configured
163    /// port, Zakura will use the default port for the configured `network`.
164    ///
165    /// `address` can be an IP address or a DNS name. DNS names are
166    /// only resolved once, when Zakura starts up.
167    ///
168    /// By default, Zakura listens on `[::]` (all IPv6 and IPv4 addresses).
169    /// This enables dual-stack support, accepting both IPv4 and IPv6 connections.
170    ///
171    /// If a specific listener address is configured, Zakura will advertise
172    /// it to other nodes. But by default, Zakura uses an unspecified address
173    /// ("\[::\]:port"), which is not advertised to other nodes.
174    ///
175    /// Zakura does not currently support:
176    /// - [Advertising a different external IP address #1890](https://github.com/ZcashFoundation/zebra/issues/1890), or
177    /// - [Auto-discovering its own external IP address #1893](https://github.com/ZcashFoundation/zebra/issues/1893).
178    ///
179    /// However, other Zakura instances compensate for unspecified or incorrect
180    /// listener addresses by adding the external IP addresses of peers to
181    /// their address books.
182    pub listen_addr: SocketAddr,
183
184    /// The external address of this node if any.
185    ///
186    /// Zakura binds to `listen_addr`, but this can be an internal address if the node
187    /// is behind a firewall, load balancer or NAT. This field can be used to
188    /// advertise a different address to peers making it possible to receive inbound
189    /// connections and contribute to the P2P network from behind a firewall, load balancer, or NAT.
190    pub external_addr: Option<SocketAddr>,
191
192    /// The network to connect to.
193    pub network: Network,
194
195    /// A list of initial peers for the peerset when operating on
196    /// mainnet.
197    pub initial_mainnet_peers: IndexSet<String>,
198
199    /// A list of initial peers for the peerset when operating on
200    /// testnet.
201    pub initial_testnet_peers: IndexSet<String>,
202
203    /// An optional root directory for storing cached peer address data.
204    ///
205    /// # Configuration
206    ///
207    /// Set to:
208    /// - `true` to read and write peer addresses to disk using the default cache path,
209    /// - `false` to disable reading and writing peer addresses to disk,
210    /// - `'/custom/cache/directory'` to read and write peer addresses to a custom directory.
211    ///
212    /// By default, all Zakura instances run by the same user will share a single peer cache.
213    /// If you use a custom cache path, you might also want to change `state.cache_dir`.
214    ///
215    /// # Functionality
216    ///
217    /// The peer cache is a list of the addresses of some recently useful peers.
218    ///
219    /// For privacy reasons, the cache does *not* include any other information about peers,
220    /// such as when they were connected to the node.
221    ///
222    /// Deleting or modifying the peer cache can impact your node's:
223    /// - reliability: if DNS or the Zcash DNS seeders are unavailable or broken
224    /// - security: if DNS is compromised with malicious peers
225    ///
226    /// If you delete it, Zakura will replace it with a fresh set of peers from the DNS seeders.
227    ///
228    /// # Defaults
229    ///
230    /// The default directory is platform dependent, based on
231    /// [`dirs::cache_dir()`](https://docs.rs/dirs/3.0.1/dirs/fn.cache_dir.html):
232    ///
233    /// |Platform | Value                                           | Example                              |
234    /// | ------- | ----------------------------------------------- | ------------------------------------ |
235    /// | Linux   | `$XDG_CACHE_HOME/zakura` or `$HOME/.cache/zakura` | `/home/alice/.cache/zakura`           |
236    /// | macOS   | `$HOME/Library/Caches/zakura`                    | `/Users/Alice/Library/Caches/zakura`  |
237    /// | Windows | `{FOLDERID_LocalAppData}\zakura`                 | `C:\Users\Alice\AppData\Local\zakura` |
238    /// | Other   | `std::env::current_dir()/cache/zakura`           | `/cache/zakura`                       |
239    ///
240    /// # Security
241    ///
242    /// If you are running Zakura with elevated permissions ("root"), create the
243    /// directory for this file before running Zakura, and make sure the Zakura user
244    /// account has exclusive access to that directory, and other users can't modify
245    /// its parent directories.
246    ///
247    /// # Implementation Details
248    ///
249    /// Each network has a separate peer list, which is updated regularly from the current
250    /// address book. These lists are stored in `network/mainnet.peers` and
251    /// `network/testnet.peers` files, underneath the `cache_dir` path.
252    ///
253    /// Previous peer lists are automatically loaded at startup, and used to populate the
254    /// initial peer set and address book.
255    pub cache_dir: CacheDir,
256
257    /// The directory for long-term network identity secrets.
258    ///
259    /// The auto-generated Zakura iroh identity key is stored under this
260    /// directory as `<network>.zakura-iroh-secret-key`. Keep this directory
261    /// outside state or cache snapshot paths, or snapshots can clone the node's
262    /// long-term P2P identity.
263    ///
264    /// The default is `~/.zakura`.
265    pub identity_dir: PathBuf,
266
267    /// An optional persistent iroh secret key for Zakura P2P identity.
268    ///
269    /// This is reserved for Zakura endpoint construction. If unset, a future Zakura endpoint
270    /// implementation will generate an ed25519 iroh [`SecretKey`] on first use
271    /// and persist it under [`identity_dir`](Self::identity_dir), outside Zakura's
272    /// cache and state directories by default.
273    ///
274    /// This value is not used by the legacy TCP peer set.
275    pub zakura_node_secret_key: Option<ZakuraNodeSecretKey>,
276
277    /// The peer-to-peer stack Zakura runs.
278    ///
279    /// | `zakura.toml` value | Stack |
280    /// |---|---|
281    /// | `"default"` | The configured network's binary default |
282    /// | `"legacy"` | The legacy TCP Zcash P2P stack only |
283    /// | `"zakura"` | The experimental native Zakura P2P v2 stack only |
284    /// | `"dual"` | Both stacks, enabling experimental v2 with legacy fallback |
285    ///
286    /// Leave this at `"default"` so Zakura can change the per-network default during upgrades.
287    /// See [`P2pStack::resolve`] for the current defaults, and [`legacy_p2p`](Self::legacy_p2p)
288    /// and [`v2_p2p`](Self::v2_p2p) for the resolved stack.
289    pub p2p_stack: P2pStack,
290
291    /// Native Zakura endpoint, connection, and bootstrap settings.
292    ///
293    /// When [`v2_p2p`](Self::v2_p2p) is false, these settings are parsed but no iroh endpoint is
294    /// started. The total intended connection budget is roughly
295    /// `peerset_initial_target_size + zakura.max_connections`; tune both together.
296    pub zakura: ZakuraConfig,
297
298    /// The initial target size for the peer set.
299    ///
300    /// Also used to limit the number of inbound and outbound connections made by Zakura,
301    /// and the size of the cached peer list.
302    ///
303    /// If you have a slow network connection, and Zakura is having trouble
304    /// syncing, try reducing the peer set size. You can also reduce the peer
305    /// set size to reduce Zakura's bandwidth usage.
306    pub peerset_initial_target_size: usize,
307
308    /// How frequently we attempt to crawl the network to discover new peer
309    /// addresses.
310    ///
311    /// Zakura asks its connected peers for more peer addresses:
312    /// - regularly, every time `crawl_new_peer_interval` elapses, and
313    /// - if the peer set is busy, and there aren't any peer addresses for the
314    ///   next connection attempt.
315    #[serde(with = "humantime_serde")]
316    pub crawl_new_peer_interval: Duration,
317
318    /// The maximum number of legacy TCP peer connections Zakura will keep for a given IP address
319    /// before it drops any additional legacy peer connections with that IP.
320    ///
321    /// The default and minimum value are 1.
322    ///
323    /// Zakura uses [`ZakuraConfig::max_connections_per_ip`] for native v2 admission.
324    ///
325    /// # Security
326    ///
327    /// Increasing this config above 1 reduces Zakura's network security.
328    ///
329    /// If this config is greater than 1, Zakura can initiate multiple outbound handshakes to the same
330    /// IP address.
331    ///
332    /// This config does not currently limit the number of inbound connections that Zakura will accept
333    /// from the same IP address.
334    ///
335    /// If Zakura makes multiple inbound or outbound connections to the same IP, they will be dropped
336    /// after the handshake, but before adding them to the peer set. The total numbers of inbound and
337    /// outbound connections are also limited to a multiple of `peerset_initial_target_size`.
338    pub max_connections_per_ip: usize,
339
340    /// Exposes legacy peer IP addresses in peer activity logs, structured trace files, and
341    /// Prometheus metric labels. This includes connected peers and candidate or address book
342    /// entries.
343    ///
344    /// Literal addresses supplied in the node configuration can appear in startup logs and
345    /// `seed` labels regardless of this setting.
346    /// If `trace_dir` is configured in `[network.zakura]`, legacy sync diagnostics can write
347    /// unredacted addresses to `legacy_sync.jsonl`.
348    ///
349    /// # Security
350    ///
351    /// Enabling this setting reveals peer topology in logs and trace files, and can create
352    /// high-cardinality metric series. Restrict access to logs, trace directories, the metrics
353    /// endpoint, and downstream monitoring systems.
354    pub expose_peer_addresses: bool,
355}
356
357impl Config {
358    /// The maximum number of outbound connections that Zakura will open at the same time.
359    /// When this limit is reached, Zakura stops opening outbound connections.
360    ///
361    /// # Security
362    ///
363    /// See the note at [`INBOUND_PEER_LIMIT_MULTIPLIER`].
364    ///
365    /// # Performance
366    ///
367    /// Zakura's peer set should be limited to a reasonable size,
368    /// to avoid queueing too many in-flight block downloads.
369    /// A large queue of in-flight block downloads can choke a
370    /// constrained local network connection.
371    ///
372    /// We assume that Zakura nodes have at least 10 Mbps bandwidth.
373    /// Therefore, a maximum-sized block can take up to 2 seconds to
374    /// download. So the initial outbound peer set adds up to 100 seconds worth
375    /// of blocks to the queue. If Zakura has reached its outbound peer limit,
376    /// that adds an extra 200 seconds of queued blocks.
377    ///
378    /// But the peer set for slow nodes is typically much smaller, due to
379    /// the handshake RTT timeout. And Zakura responds to inbound request
380    /// overloads by dropping peer connections.
381    pub fn peerset_outbound_connection_limit(&self) -> usize {
382        self.peerset_initial_target_size * OUTBOUND_PEER_LIMIT_MULTIPLIER
383    }
384
385    /// The maximum number of inbound connections that Zakura will accept at the same time.
386    /// When this limit is reached, Zakura drops new inbound connections,
387    /// without handshaking on them.
388    ///
389    /// # Security
390    ///
391    /// See the note at [`INBOUND_PEER_LIMIT_MULTIPLIER`].
392    pub fn peerset_inbound_connection_limit(&self) -> usize {
393        self.peerset_initial_target_size * INBOUND_PEER_LIMIT_MULTIPLIER
394    }
395
396    /// The maximum number of inbound and outbound connections that Zakura will have
397    /// at the same time.
398    pub fn peerset_total_connection_limit(&self) -> usize {
399        self.peerset_outbound_connection_limit() + self.peerset_inbound_connection_limit()
400    }
401
402    /// Returns the initial seed peer hostnames for the configured network.
403    pub fn initial_peer_hostnames(&self) -> IndexSet<String> {
404        match &self.network {
405            Network::Mainnet => self.initial_mainnet_peers.clone(),
406            Network::Testnet(_params) => self.initial_testnet_peers.clone(),
407        }
408    }
409
410    /// Resolve initial seed peer IP addresses, based on the configured network,
411    /// and load cached peers from disk, if available.
412    ///
413    /// # Panics
414    ///
415    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
416    pub async fn initial_peers(&self) -> HashSet<PeerSocketAddr> {
417        // TODO: do DNS and disk in parallel if startup speed becomes important
418        let dns_peers = Config::resolve_peers(
419            &self.initial_peer_hostnames().iter().cloned().collect(),
420            self.expose_peer_addresses,
421        )
422        .await;
423
424        if self.network.is_regtest() {
425            // Only return local peer addresses and skip loading the peer cache on Regtest.
426            dns_peers
427                .into_iter()
428                .filter(PeerSocketAddr::is_localhost)
429                .collect()
430        } else {
431            // Ignore disk errors because the cache is optional and the method already logs them.
432            let disk_peers = self.load_peer_cache().await.unwrap_or_default();
433
434            dns_peers.into_iter().chain(disk_peers).collect()
435        }
436    }
437
438    /// Concurrently resolves `peers` into zero or more IP addresses, with a
439    /// timeout of a few seconds on each DNS request.
440    ///
441    /// If DNS resolution fails or times out for all peers, continues retrying
442    /// until at least one peer is found.
443    async fn resolve_peers(
444        peers: &HashSet<String>,
445        expose_peer_addresses: bool,
446    ) -> HashSet<PeerSocketAddr> {
447        use futures::stream::StreamExt;
448
449        if peers.is_empty() {
450            warn!(
451                "no initial peers in the network config. \
452                 Hint: you must configure at least one peer IP or DNS seeder to run Zakura, \
453                 give it some previously cached peer IP addresses on disk, \
454                 or make sure Zakura's listener port gets inbound connections."
455            );
456            return HashSet::new();
457        }
458
459        loop {
460            // We retry each peer individually, as well as retrying if there are
461            // no peers in the combined list. DNS failures are correlated, so all
462            // peers can fail DNS, leaving Zakura with a small list of custom IP
463            // address peers. Individual retries avoid this issue.
464            let peer_addresses = peers
465                .iter()
466                .map(|s| {
467                    Config::resolve_host(s, MAX_SINGLE_SEED_PEER_DNS_RETRIES, expose_peer_addresses)
468                })
469                .collect::<futures::stream::FuturesUnordered<_>>()
470                .concat()
471                .await;
472
473            if peer_addresses.is_empty() {
474                tracing::info!(
475                    ?peers,
476                    ?peer_addresses,
477                    "empty peer list after DNS resolution, retrying after {} seconds",
478                    DNS_LOOKUP_TIMEOUT.as_secs(),
479                );
480                tokio::time::sleep(DNS_LOOKUP_TIMEOUT).await;
481            } else {
482                return peer_addresses;
483            }
484        }
485    }
486
487    /// Resolves `host` into zero or more IP addresses, retrying up to
488    /// `max_retries` times.
489    ///
490    /// If DNS continues to fail, returns an empty list of addresses.
491    ///
492    /// # Panics
493    ///
494    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
495    async fn resolve_host(
496        host: &str,
497        max_retries: usize,
498        expose_peer_addresses: bool,
499    ) -> HashSet<PeerSocketAddr> {
500        for retries in 0..=max_retries {
501            if let Ok(addresses) = Config::resolve_host_once(host, expose_peer_addresses).await {
502                return addresses;
503            }
504
505            if retries < max_retries {
506                tracing::info!(
507                    ?host,
508                    previous_attempts = ?(retries + 1),
509                    "Waiting {DNS_LOOKUP_TIMEOUT:?} to retry seed peer DNS resolution",
510                );
511                tokio::time::sleep(DNS_LOOKUP_TIMEOUT).await;
512            } else {
513                tracing::info!(
514                    ?host,
515                    attempts = ?(retries + 1),
516                    "Seed peer DNS resolution failed, checking for addresses from other seed peers",
517                );
518            }
519        }
520
521        HashSet::new()
522    }
523
524    /// Resolves `host` into zero or more IP addresses.
525    ///
526    /// If `host` is a DNS name, performs DNS resolution with a timeout of a few seconds.
527    /// If DNS resolution fails or times out, returns an error.
528    ///
529    /// # Panics
530    ///
531    /// If a configured address is an invalid [`SocketAddr`] or DNS name.
532    async fn resolve_host_once(
533        host: &str,
534        expose_peer_addresses: bool,
535    ) -> Result<HashSet<PeerSocketAddr>, BoxError> {
536        let fut = tokio::net::lookup_host(host);
537        let fut = tokio::time::timeout(DNS_LOOKUP_TIMEOUT, fut);
538
539        match fut.await {
540            Ok(Ok(ip_addrs)) => {
541                let ip_addrs: Vec<PeerSocketAddr> = ip_addrs.map(canonical_peer_addr).collect();
542
543                // This log is needed for user debugging, but it's annoying during tests.
544                #[cfg(not(test))]
545                info!(seed = ?host, remote_ip_count = ?ip_addrs.len(), "resolved seed peer IP addresses");
546                #[cfg(test)]
547                debug!(seed = ?host, remote_ip_count = ?ip_addrs.len(), "resolved seed peer IP addresses");
548
549                for ip in &ip_addrs {
550                    // Count each initial peer, recording the seed config and resolved IP address.
551                    //
552                    // If an IP is returned by multiple seeds,
553                    // each duplicate adds 1 to the initial peer count.
554                    // (But we only make one initial connection attempt to each IP.)
555                    metrics::counter!(
556                        "zcash.net.peers.initial",
557                        "seed" => host.to_string(),
558                        "remote_ip" => ip.addr_label(expose_peer_addresses)
559                    )
560                    .increment(1);
561                }
562
563                Ok(ip_addrs.into_iter().collect())
564            }
565            Ok(Err(e)) if e.kind() == ErrorKind::InvalidInput => {
566                // TODO: add testnet/mainnet ports, like we do with the listener address
567                panic!(
568                    "Invalid peer IP address in Zakura config: addresses must have ports:\n\
569                     resolving {host:?} returned {e:?}"
570                );
571            }
572            Ok(Err(e)) => {
573                tracing::info!(?host, ?e, "DNS error resolving peer IP addresses");
574                Err(e.into())
575            }
576            Err(e) => {
577                tracing::info!(?host, ?e, "DNS timeout resolving peer IP addresses");
578                Err(e.into())
579            }
580        }
581    }
582
583    /// Returns the addresses in the peer list cache file, if available.
584    pub async fn load_peer_cache(&self) -> io::Result<HashSet<PeerSocketAddr>> {
585        let Some(peer_cache_file) = self.cache_dir.peer_cache_file_path(&self.network) else {
586            return Ok(HashSet::new());
587        };
588
589        let peer_list = match fs::read_to_string(&peer_cache_file).await {
590            Ok(peer_list) => peer_list,
591            Err(peer_list_error) => {
592                // We expect that the cache will be missing for new Zakura installs
593                if peer_list_error.kind() == ErrorKind::NotFound {
594                    return Ok(HashSet::new());
595                } else {
596                    info!(
597                        ?peer_list_error,
598                        "could not load cached peer list, using default seed peers"
599                    );
600                    return Err(peer_list_error);
601                }
602            }
603        };
604
605        // Skip and log addresses that don't parse, and automatically deduplicate using the HashSet.
606        // (These issues shouldn't happen unless users modify the file.)
607        let peer_list: HashSet<PeerSocketAddr> = peer_list
608            .lines()
609            .filter_map(|peer| {
610                peer.parse()
611                    .map_err(|peer_parse_error| {
612                        info!(
613                            ?peer_parse_error,
614                            "invalid peer address in cached peer list, skipping"
615                        );
616                        peer_parse_error
617                    })
618                    .ok()
619            })
620            .collect();
621
622        // This log is needed for user debugging, but it's annoying during tests.
623        #[cfg(not(test))]
624        info!(
625            cached_ip_count = ?peer_list.len(),
626            ?peer_cache_file,
627            "loaded cached peer IP addresses"
628        );
629        #[cfg(test)]
630        debug!(
631            cached_ip_count = ?peer_list.len(),
632            ?peer_cache_file,
633            "loaded cached peer IP addresses"
634        );
635
636        for ip in &peer_list {
637            // Count each initial peer, recording the cache file and loaded IP address.
638            //
639            // If an IP is returned by DNS seeders and the cache,
640            // each duplicate adds 1 to the initial peer count.
641            // (But we only make one initial connection attempt to each IP.)
642            metrics::counter!(
643                "zcash.net.peers.initial",
644                "cache" => peer_cache_file.display().to_string(),
645                "remote_ip" => ip.addr_label(self.expose_peer_addresses)
646            )
647            .increment(1);
648        }
649
650        Ok(peer_list)
651    }
652
653    /// Atomically writes a new `peer_list` to the peer list cache file, if configured.
654    /// If the list is empty, keeps the previous cache file.
655    ///
656    /// Also creates the peer cache directory, if it doesn't already exist.
657    ///
658    /// Atomic writes avoid corrupting the cache if Zakura panics or crashes, or if multiple Zakura
659    /// instances try to read and write the same cache file.
660    pub async fn update_peer_cache(&self, peer_list: HashSet<PeerSocketAddr>) -> io::Result<()> {
661        let Some(peer_cache_file) = self.cache_dir.peer_cache_file_path(&self.network) else {
662            return Ok(());
663        };
664
665        if peer_list.is_empty() {
666            info!(
667                ?peer_cache_file,
668                "cacheable peer list was empty, keeping previous cache"
669            );
670            return Ok(());
671        }
672
673        let selected_peers: Vec<PeerSocketAddr> = peer_list
674            .iter()
675            .take(MAX_PEER_DISK_CACHE_SIZE)
676            .copied()
677            .collect();
678
679        // Turn IP addresses into unredacted strings so they remain reconnectable.
680        let mut peer_list: Vec<String> = selected_peers
681            .iter()
682            .map(|peer| peer.remove_socket_addr_privacy().to_string())
683            .collect();
684        // # Privacy
685        //
686        // Sort to destroy any peer order, which could leak peer connection times.
687        // (Currently the HashSet argument does this as well.)
688        peer_list.sort();
689        // Make a newline-separated list
690        let peer_data = peer_list.join("\n");
691
692        // Write the peer cache file atomically so the cache is not corrupted if Zakura shuts down
693        // or crashes.
694        let span = Span::current();
695        let write_result = tokio::task::spawn_blocking(move || {
696            span.in_scope(move || atomic_write(peer_cache_file, peer_data.as_bytes()))
697        })
698        .await
699        .expect("could not write the peer cache file")?;
700
701        match write_result {
702            Ok(peer_cache_file) => {
703                info!(
704                    cached_ip_count = ?peer_list.len(),
705                    ?peer_cache_file,
706                    "updated cached peer IP addresses"
707                );
708
709                for ip in &selected_peers {
710                    metrics::counter!(
711                        "zcash.net.peers.cache",
712                        "cache" => peer_cache_file.display().to_string(),
713                        "remote_ip" => ip.addr_label(self.expose_peer_addresses)
714                    )
715                    .increment(1);
716                }
717
718                Ok(())
719            }
720            Err(error) => Err(error.error),
721        }
722    }
723
724    /// Resolves the Zakura native iroh [`SecretKey`] for this node, persisting a
725    /// freshly generated key on first use so the node keeps a stable
726    /// [`NodeId`](iroh::NodeId) across restarts.
727    ///
728    /// Resolution order:
729    /// 1. If [`zakura_node_secret_key`](Self::zakura_node_secret_key) is configured,
730    ///    it is parsed and used verbatim. An unparsable value is a hard error.
731    /// 2. Otherwise, the persisted key file under
732    ///    [`identity_dir`](Self::identity_dir) is loaded; when it is missing or
733    ///    unreadable a fresh key is generated and written atomically with
734    ///    owner-only (`0o600`) permissions, so every later startup reuses the
735    ///    same identity.
736    /// 3. If the key cannot be persisted, the freshly generated identity is used
737    ///    ephemerally for this run.
738    ///
739    /// # Security
740    ///
741    /// The persisted key file is the node's long-term private identity. It is
742    /// written outside the cache and state directories and restricted to owner
743    /// read/write on Unix.
744    pub fn zakura_secret_key(&self) -> Result<SecretKey, ZakuraSecretKeyError> {
745        if let Some(secret) = &self.zakura_node_secret_key {
746            return SecretKey::from_str(secret.expose_secret())
747                .map_err(|_| ZakuraSecretKeyError::InvalidConfigured);
748        }
749
750        let key_file = zakura_secret_key_file_path(&self.identity_dir, &self.network);
751
752        Ok(load_or_generate_zakura_secret_key(&key_file))
753    }
754
755    /// Returns `true` if Zakura should run the legacy TCP Zcash P2P listener, initial peer
756    /// dialing, and peer crawler on the configured network.
757    pub fn legacy_p2p(&self) -> bool {
758        self.p2p_stack.resolve(&self.network).runs_legacy()
759    }
760
761    /// Returns `true` if Zakura should run the native Zakura P2P v2 endpoint on the configured
762    /// network, advertise the P2P v2 service bit during the legacy Zcash handshake, and route
763    /// mutually capable peers to the Zakura upgrade hook.
764    pub fn v2_p2p(&self) -> bool {
765        self.p2p_stack.resolve(&self.network).runs_zakura()
766    }
767
768    /// Builds a network config for tests with [`p2p_stack`](Self::p2p_stack) pinned to
769    /// `p2p_stack`, so the stack is never re-derived from the network's binary defaults.
770    ///
771    /// Override other fields with struct-update syntax, e.g.
772    /// `Config { network, ..Config::for_test(P2pStack::Dual) }`.
773    ///
774    /// # Panics
775    ///
776    /// If `p2p_stack` is [`P2pStack::Default`]: a test that doesn't pin its stack silently
777    /// changes behaviour when the per-network defaults change.
778    #[cfg(any(test, feature = "proptest-impl"))]
779    pub fn for_test(p2p_stack: P2pStack) -> Config {
780        assert_ne!(
781            p2p_stack,
782            P2pStack::Default,
783            "tests must pin an explicit P2P stack",
784        );
785
786        Config {
787            p2p_stack,
788            ..Config::default()
789        }
790    }
791}
792
793/// Loads a persisted Zakura secret key from `key_file`, or generates, persists, and
794/// returns a fresh key when the file is absent or cannot be parsed.
795///
796/// I/O failures are logged and downgraded to an ephemeral key for this run, so a
797/// read-only or full cache directory never prevents the node from starting.
798fn load_or_generate_zakura_secret_key(key_file: &Path) -> SecretKey {
799    match std::fs::read_to_string(key_file) {
800        Ok(contents) => match SecretKey::from_str(contents.trim()) {
801            Ok(secret_key) => return secret_key,
802            Err(_) => warn!(
803                ?key_file,
804                "ignoring unparsable Zakura node secret key file; regenerating a new identity"
805            ),
806        },
807        Err(error) if error.kind() == ErrorKind::NotFound => {}
808        Err(error) => warn!(
809            ?error,
810            ?key_file,
811            "could not read Zakura node secret key file; regenerating a new identity"
812        ),
813    }
814
815    let secret_key = SecretKey::generate(OsRng);
816    persist_zakura_secret_key(key_file, &secret_key);
817    secret_key
818}
819
820/// Atomically writes `secret_key` to `key_file` as lowercase hex and restricts
821/// the file to owner-only access. Persistence failures are logged but not fatal.
822fn persist_zakura_secret_key(key_file: &Path, secret_key: &SecretKey) {
823    let encoded = hex::encode(secret_key.to_bytes());
824
825    match atomic_write(key_file.to_path_buf(), encoded.as_bytes()) {
826        Ok(Ok(path)) => {
827            if let Err(error) = restrict_secret_key_file_permissions(&path) {
828                warn!(
829                    ?error,
830                    ?path,
831                    "persisted Zakura node secret key but could not restrict its permissions"
832                );
833            } else {
834                info!(?path, "persisted a new Zakura node secret key");
835            }
836        }
837        Ok(Err(error)) => warn!(
838            ?error,
839            ?key_file,
840            "could not persist Zakura node secret key; using an ephemeral identity this run"
841        ),
842        Err(error) => warn!(
843            ?error,
844            ?key_file,
845            "could not persist Zakura node secret key; using an ephemeral identity this run"
846        ),
847    }
848}
849
850/// Restricts the persisted secret key file to owner read/write (`0o600`) on Unix.
851#[cfg(unix)]
852fn restrict_secret_key_file_permissions(path: &Path) -> io::Result<()> {
853    use std::os::unix::fs::PermissionsExt;
854
855    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
856}
857
858/// File permissions are not restricted on non-Unix platforms.
859#[cfg(not(unix))]
860fn restrict_secret_key_file_permissions(_path: &Path) -> io::Result<()> {
861    Ok(())
862}
863
864impl Default for Config {
865    fn default() -> Config {
866        let mainnet_peers = [
867            "dnsseed.str4d.xyz:8233",
868            "dnsseed.z.cash:8233",
869            "mainnet.seeder.shieldedinfra.net:8233",
870            "mainnet.seeder.zfnd.org:8233",
871        ]
872        .iter()
873        .map(|&s| String::from(s))
874        .collect();
875
876        let testnet_peers = [
877            "dnsseed.testnet.z.cash:18233",
878            "testnet.seeder.zfnd.org:18233",
879        ]
880        .iter()
881        .map(|&s| String::from(s))
882        .collect();
883
884        Config {
885            listen_addr: "[::]:8233"
886                .parse()
887                .expect("Hardcoded address should be parseable"),
888            external_addr: None,
889            network: Network::Mainnet,
890            initial_mainnet_peers: mainnet_peers,
891            initial_testnet_peers: testnet_peers,
892            cache_dir: CacheDir::default(),
893            identity_dir: default_network_identity_dir(),
894            zakura_node_secret_key: None,
895            p2p_stack: P2pStack::Default,
896            zakura: ZakuraConfig::default(),
897            crawl_new_peer_interval: DEFAULT_CRAWL_NEW_PEER_INTERVAL,
898
899            // # Security
900            //
901            // The default peerset target size should be large enough to ensure
902            // nodes have a reliable set of peers.
903            //
904            // But Zakura should only make a small number of initial outbound connections,
905            // so that idle peers don't use too many connection slots.
906            peerset_initial_target_size: DEFAULT_PEERSET_INITIAL_TARGET_SIZE,
907            max_connections_per_ip: DEFAULT_MAX_CONNS_PER_IP,
908            expose_peer_addresses: false,
909        }
910    }
911}
912
913/// Maps the deprecated `legacy_p2p` and `v2_p2p` booleans onto a [`P2pStack`], so configs
914/// written before `p2p_stack` keep loading.
915///
916/// Setting `p2p_stack` alongside either deprecated field is rejected rather than resolved by
917/// precedence, because the settings can contradict each other.
918fn p2p_stack_from_config<'de, D>(
919    p2p_stack: Option<P2pStack>,
920    legacy_p2p: Option<bool>,
921    v2_p2p: Option<bool>,
922) -> Result<P2pStack, D::Error>
923where
924    D: Deserializer<'de>,
925{
926    match (p2p_stack, legacy_p2p, v2_p2p) {
927        (Some(_), Some(_), _) | (Some(_), _, Some(_)) => Err(de::Error::custom(
928            "network.p2p_stack can't be combined with the deprecated network.legacy_p2p or \
929             network.v2_p2p settings; use network.p2p_stack on its own",
930        )),
931
932        (Some(p2p_stack), None, None) => Ok(p2p_stack),
933
934        (None, None, None) => Ok(P2pStack::Default),
935
936        (None, legacy_p2p, v2_p2p) => match (legacy_p2p.unwrap_or(true), v2_p2p.unwrap_or(true)) {
937            (true, true) => Ok(P2pStack::Dual),
938            (true, false) => Ok(P2pStack::Legacy),
939            (false, true) => Ok(P2pStack::Zakura),
940            (false, false) => Err(de::Error::custom(
941                "network.legacy_p2p and network.v2_p2p are both false, which disables all \
942                 peer-to-peer networking; set network.p2p_stack instead",
943            )),
944        },
945    }
946}
947
948#[derive(Serialize, Deserialize)]
949#[serde(deny_unknown_fields)]
950struct DTestnetParameters {
951    network_name: Option<String>,
952    network_magic: Option<[u8; 4]>,
953    slow_start_interval: Option<u32>,
954    target_difficulty_limit: Option<String>,
955    disable_pow: Option<bool>,
956    genesis_hash: Option<String>,
957    activation_heights: Option<ConfiguredActivationHeights>,
958    pre_nu6_funding_streams: Option<ConfiguredFundingStreams>,
959    post_nu6_funding_streams: Option<ConfiguredFundingStreams>,
960    funding_streams: Option<Vec<ConfiguredFundingStreams>>,
961    pre_blossom_halving_interval: Option<u32>,
962    lockbox_disbursements: Option<Vec<ConfiguredLockboxDisbursement>>,
963    #[serde(default)]
964    checkpoints: ConfiguredCheckpoints,
965    /// If `true`, automatically repeats configured funding stream addresses to fill
966    /// all required periods.
967    extend_funding_stream_addresses_as_required: Option<bool>,
968    /// Height at which the soft fork that temporarily disables Orchard actions activates.
969    ///
970    /// If unset, the default activation height for the network is used; the soft fork
971    /// cannot be disabled via configuration.
972    temporary_orchard_disabling_soft_fork_height: Option<u32>,
973}
974
975/// Network configuration used during deserialization.
976#[derive(Serialize, Deserialize)]
977#[serde(untagged)]
978enum DNetwork {
979    DefaultForKind(NetworkKind),
980    ConfiguredRegtest {
981        params: Box<DTestnetParameters>,
982
983        #[serde(default, skip_serializing)]
984        regtest: Option<bool>,
985    },
986    ConfiguredTestnet(Box<DTestnetParameters>),
987}
988
989impl Default for DNetwork {
990    fn default() -> Self {
991        DNetwork::DefaultForKind(NetworkKind::Mainnet)
992    }
993}
994
995#[derive(Serialize, Deserialize)]
996#[serde(deny_unknown_fields, default)]
997struct DConfig {
998    listen_addr: String,
999    external_addr: Option<String>,
1000    network: DNetwork,
1001
1002    /// Legacy testnet parameters, kept for backwards compatibility.
1003    #[serde(default, skip_serializing_if = "Option::is_none")]
1004    testnet_parameters: Option<DTestnetParameters>,
1005
1006    initial_mainnet_peers: IndexSet<String>,
1007    initial_testnet_peers: IndexSet<String>,
1008    cache_dir: CacheDir,
1009    identity_dir: PathBuf,
1010    #[serde(default, skip_serializing)]
1011    zakura_node_secret_key: Option<ZakuraNodeSecretKey>,
1012    #[serde(default, skip_serializing_if = "Option::is_none")]
1013    p2p_stack: Option<P2pStack>,
1014    /// Deprecated, superseded by `p2p_stack`. Accepted so configs written for older releases
1015    /// keep loading, and never written back out.
1016    #[serde(default, skip_serializing)]
1017    legacy_p2p: Option<bool>,
1018    /// Deprecated, superseded by `p2p_stack`. See `legacy_p2p`.
1019    #[serde(default, alias = "enable_p2p_v2", skip_serializing)]
1020    v2_p2p: Option<bool>,
1021    zakura: ZakuraConfig,
1022    peerset_initial_target_size: usize,
1023    #[serde(alias = "new_peer_interval", with = "humantime_serde")]
1024    crawl_new_peer_interval: Duration,
1025    max_connections_per_ip: Option<usize>,
1026    expose_peer_addresses: bool,
1027}
1028
1029impl Default for DConfig {
1030    fn default() -> Self {
1031        let config = Config::default();
1032        Self {
1033            listen_addr: "[::]".to_string(),
1034            external_addr: None,
1035            network: Default::default(),
1036            testnet_parameters: None,
1037            initial_mainnet_peers: config.initial_mainnet_peers,
1038            initial_testnet_peers: config.initial_testnet_peers,
1039            cache_dir: config.cache_dir,
1040            identity_dir: config.identity_dir,
1041            zakura_node_secret_key: config.zakura_node_secret_key,
1042            p2p_stack: Some(config.p2p_stack),
1043            legacy_p2p: None,
1044            v2_p2p: None,
1045            zakura: config.zakura,
1046            peerset_initial_target_size: config.peerset_initial_target_size,
1047            crawl_new_peer_interval: config.crawl_new_peer_interval,
1048            max_connections_per_ip: Some(config.max_connections_per_ip),
1049            expose_peer_addresses: config.expose_peer_addresses,
1050        }
1051    }
1052}
1053
1054impl From<Arc<testnet::Parameters>> for DTestnetParameters {
1055    fn from(params: Arc<testnet::Parameters>) -> Self {
1056        Self {
1057            network_name: Some(params.network_name().to_string()),
1058            network_magic: Some(params.network_magic().0),
1059            slow_start_interval: Some(params.slow_start_interval().0),
1060            target_difficulty_limit: Some(params.target_difficulty_limit().to_string()),
1061            disable_pow: Some(params.disable_pow()),
1062            genesis_hash: Some(params.genesis_hash().to_string()),
1063            activation_heights: Some(params.activation_heights().into()),
1064            pre_nu6_funding_streams: None,
1065            post_nu6_funding_streams: None,
1066            funding_streams: Some(params.funding_streams().iter().map(Into::into).collect()),
1067            pre_blossom_halving_interval: Some(
1068                params
1069                    .pre_blossom_halving_interval()
1070                    .try_into()
1071                    .expect("should convert"),
1072            ),
1073            lockbox_disbursements: Some(
1074                params
1075                    .lockbox_disbursements()
1076                    .into_iter()
1077                    .map(Into::into)
1078                    .collect(),
1079            ),
1080            checkpoints: if params.checkpoints() == testnet::Parameters::default().checkpoints() {
1081                ConfiguredCheckpoints::Default(true)
1082            } else {
1083                params.checkpoints().into()
1084            },
1085            extend_funding_stream_addresses_as_required: None,
1086            temporary_orchard_disabling_soft_fork_height: params
1087                .temporary_orchard_disabling_soft_fork_height()
1088                .map(|height| height.0),
1089        }
1090    }
1091}
1092
1093impl From<Config> for DConfig {
1094    fn from(
1095        Config {
1096            listen_addr,
1097            external_addr,
1098            network,
1099            initial_mainnet_peers,
1100            initial_testnet_peers,
1101            cache_dir,
1102            identity_dir,
1103            zakura_node_secret_key,
1104            p2p_stack,
1105            zakura,
1106            peerset_initial_target_size,
1107            crawl_new_peer_interval,
1108            max_connections_per_ip,
1109            expose_peer_addresses,
1110        }: Config,
1111    ) -> Self {
1112        let dnetwork = match network.kind() {
1113            NetworkKind::Testnet => match network
1114                .parameters()
1115                .filter(|params| !params.is_default_testnet())
1116                .map(Into::into)
1117            {
1118                Some(params) => DNetwork::ConfiguredTestnet(Box::new(params)),
1119                None => DNetwork::DefaultForKind(NetworkKind::Testnet),
1120            },
1121
1122            NetworkKind::Regtest => match network.parameters().map(Into::into) {
1123                Some(params) => DNetwork::ConfiguredRegtest {
1124                    params: Box::new(params),
1125                    regtest: Some(true),
1126                },
1127                None => DNetwork::DefaultForKind(NetworkKind::Regtest),
1128            },
1129
1130            other_kind => DNetwork::DefaultForKind(other_kind),
1131        };
1132
1133        DConfig {
1134            listen_addr: listen_addr.to_string(),
1135            external_addr: external_addr.map(|addr| addr.to_string()),
1136            network: dnetwork,
1137            testnet_parameters: None,
1138            initial_mainnet_peers,
1139            initial_testnet_peers,
1140            cache_dir,
1141            identity_dir,
1142            zakura_node_secret_key,
1143            p2p_stack: Some(p2p_stack),
1144            legacy_p2p: None,
1145            v2_p2p: None,
1146            zakura,
1147            peerset_initial_target_size,
1148            crawl_new_peer_interval,
1149            max_connections_per_ip: Some(max_connections_per_ip),
1150            expose_peer_addresses,
1151        }
1152    }
1153}
1154
1155impl<'de> Deserialize<'de> for Config {
1156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1157    where
1158        D: Deserializer<'de>,
1159    {
1160        let DConfig {
1161            listen_addr,
1162            external_addr,
1163            network: dnetwork,
1164            testnet_parameters,
1165            initial_mainnet_peers,
1166            initial_testnet_peers,
1167            cache_dir,
1168            identity_dir,
1169            zakura_node_secret_key,
1170            p2p_stack,
1171            legacy_p2p,
1172            v2_p2p,
1173            zakura,
1174            peerset_initial_target_size,
1175            crawl_new_peer_interval,
1176            max_connections_per_ip,
1177            expose_peer_addresses,
1178        } = DConfig::deserialize(deserializer)?;
1179
1180        let p2p_stack = p2p_stack_from_config::<D>(p2p_stack, legacy_p2p, v2_p2p)?;
1181
1182        let network = match (dnetwork, testnet_parameters) {
1183            (DNetwork::ConfiguredTestnet(params), _) => {
1184                build_configured_testnet::<D>(*params, &initial_testnet_peers)?
1185            }
1186            (DNetwork::ConfiguredRegtest { params, .. }, _) => {
1187                Network::new_regtest(build_regtest_params(*params))
1188            }
1189            (DNetwork::DefaultForKind(NetworkKind::Mainnet), _) => Network::Mainnet,
1190            (DNetwork::DefaultForKind(NetworkKind::Testnet), Some(params)) => {
1191                build_configured_testnet::<D>(params, &initial_testnet_peers)?
1192            }
1193            (DNetwork::DefaultForKind(NetworkKind::Testnet), None) => {
1194                Network::new_default_testnet()
1195            }
1196            (DNetwork::DefaultForKind(NetworkKind::Regtest), Some(params)) => {
1197                Network::new_regtest(build_regtest_params(params))
1198            }
1199            (DNetwork::DefaultForKind(NetworkKind::Regtest), None) => {
1200                Network::new_regtest(Default::default())
1201            }
1202        };
1203
1204        let listen_addr = match listen_addr.parse::<SocketAddr>().or_else(|_| format!("{listen_addr}:{}", network.default_port()).parse()) {
1205            Ok(socket) => Ok(socket),
1206            Err(_) => match listen_addr.parse::<IpAddr>() {
1207                Ok(ip) => Ok(SocketAddr::new(ip, network.default_port())),
1208                Err(err) => Err(de::Error::custom(format!(
1209                    "{err}; Hint: addresses can be a IPv4, IPv6 (with brackets), or a DNS name, the port is optional"
1210                ))),
1211            },
1212        }?;
1213
1214        let external_socket_addr = if let Some(address) = &external_addr {
1215            match address.parse::<SocketAddr>().or_else(|_| format!("{address}:{}", network.default_port()).parse()) {
1216                Ok(socket) => Ok(Some(socket)),
1217                Err(_) => match address.parse::<IpAddr>() {
1218                    Ok(ip) => Ok(Some(SocketAddr::new(ip, network.default_port()))),
1219                    Err(err) => Err(de::Error::custom(format!(
1220                        "{err}; Hint: addresses can be a IPv4, IPv6 (with brackets), or a DNS name, the port is optional"
1221                    ))),
1222                },
1223            }?
1224        } else {
1225            None
1226        };
1227
1228        let [max_connections_per_ip, peerset_initial_target_size] = [
1229            ("max_connections_per_ip", max_connections_per_ip, DEFAULT_MAX_CONNS_PER_IP),
1230            // If we want Zakura to operate with no network,
1231            // we should implement a `zakurad` command that doesn't use `zakura-network`.
1232            ("peerset_initial_target_size", Some(peerset_initial_target_size), DEFAULT_PEERSET_INITIAL_TARGET_SIZE)
1233        ].map(|(field_name, non_zero_config_field, default_config_value)| {
1234            if non_zero_config_field == Some(0) {
1235                warn!(
1236                    ?field_name,
1237                    ?non_zero_config_field,
1238                    "{field_name} should be greater than 0, using default value of {default_config_value} instead"
1239                );
1240            }
1241
1242            non_zero_config_field.filter(|config_value| config_value > &0).unwrap_or(default_config_value)
1243        });
1244
1245        // Clamp too-small budgets rather than rejecting existing configurations.
1246        let mut zakura = zakura;
1247        zakura.apply_network_defaults(&network);
1248        let default_zakura_bootstrap_peers =
1249            ZakuraConfig::default_bootstrap_peers_for_network(&network);
1250        if zakura.bootstrap_peers.is_empty() {
1251            warn!(
1252                ?network,
1253                "no Zakura bootstrap peers configured; configure zakura.bootstrap_peers or make sure this node receives inbound Zakura connections"
1254            );
1255        } else if network.kind() != NetworkKind::Regtest
1256            && zakura.bootstrap_peers != default_zakura_bootstrap_peers
1257        {
1258            warn!(
1259                ?network,
1260                configured_zakura_bootstrap_peers = ?zakura.bootstrap_peers,
1261                ?default_zakura_bootstrap_peers,
1262                "configured Zakura bootstrap peers differ from the default peers for this network"
1263            );
1264        }
1265        if zakura_listens_on_loopback_with_non_loopback_bootstrap_peers(&zakura) {
1266            warn!(
1267                ?network,
1268                listen_addr = ?zakura.listen_addr,
1269                bootstrap_peers = ?zakura.bootstrap_peers,
1270                "configured Zakura listen_addr is loopback-only, but bootstrap peers use \
1271                 non-loopback addresses; native Zakura dials may fail with \
1272                 `Can't assign requested address`. Use 0.0.0.0:<port> or another routable \
1273                 interface address for public Zakura peers"
1274            );
1275        }
1276        zakura
1277            .block_sync
1278            .clamp_inflight_block_bytes_to_request_floor();
1279        zakura.block_sync.clamp_reorder_lookahead_to_floor();
1280        zakura.block_sync.validate().map_err(|error| {
1281            de::Error::custom(format!("invalid zakura.block_sync config: {error}"))
1282        })?;
1283
1284        Ok(Config {
1285            listen_addr: canonical_socket_addr(listen_addr),
1286            external_addr: external_socket_addr,
1287            network,
1288            initial_mainnet_peers,
1289            initial_testnet_peers,
1290            cache_dir,
1291            identity_dir,
1292            zakura_node_secret_key,
1293            p2p_stack,
1294            zakura,
1295            peerset_initial_target_size,
1296            crawl_new_peer_interval,
1297            max_connections_per_ip,
1298            expose_peer_addresses,
1299        })
1300    }
1301}
1302
1303fn zakura_listens_on_loopback_with_non_loopback_bootstrap_peers(zakura: &ZakuraConfig) -> bool {
1304    let Some(listen_addr) = zakura.listen_addr else {
1305        return false;
1306    };
1307
1308    listen_addr.ip().is_loopback()
1309        && zakura
1310            .bootstrap_peers
1311            .iter()
1312            .filter_map(|peer| peer.rsplit_once('@'))
1313            .filter_map(|(_node_id, addr)| addr.parse::<SocketAddr>().ok())
1314            .any(|addr| !addr.ip().is_loopback())
1315}
1316
1317/// Accepts an [`IndexSet`] of initial peers,
1318///
1319/// Returns true if any of them are the default Testnet or Mainnet initial peers.
1320fn contains_default_initial_peers(initial_peers: &IndexSet<String>) -> bool {
1321    let Config {
1322        initial_mainnet_peers: mut default_initial_peers,
1323        initial_testnet_peers: default_initial_testnet_peers,
1324        ..
1325    } = Config::default();
1326    default_initial_peers.extend(default_initial_testnet_peers);
1327
1328    initial_peers
1329        .intersection(&default_initial_peers)
1330        .next()
1331        .is_some()
1332}
1333
1334fn build_configured_testnet<'de, D>(
1335    params: DTestnetParameters,
1336    initial_testnet_peers: &IndexSet<String>,
1337) -> Result<Network, D::Error>
1338where
1339    D: Deserializer<'de>,
1340{
1341    let DTestnetParameters {
1342        network_name,
1343        network_magic,
1344        slow_start_interval,
1345        target_difficulty_limit,
1346        disable_pow,
1347        genesis_hash,
1348        activation_heights,
1349        pre_nu6_funding_streams,
1350        post_nu6_funding_streams,
1351        funding_streams,
1352        pre_blossom_halving_interval,
1353        lockbox_disbursements,
1354        checkpoints,
1355        extend_funding_stream_addresses_as_required,
1356        temporary_orchard_disabling_soft_fork_height,
1357    } = params;
1358
1359    let mut params_builder = testnet::Parameters::build();
1360
1361    if let Some(network_name) = network_name.clone() {
1362        params_builder = params_builder
1363            .with_network_name(network_name)
1364            .map_err(de::Error::custom)?
1365    }
1366
1367    if let Some(network_magic) = network_magic {
1368        params_builder = params_builder
1369            .with_network_magic(Magic(network_magic))
1370            .map_err(de::Error::custom)?;
1371    }
1372
1373    if let Some(genesis_hash) = genesis_hash {
1374        params_builder = params_builder
1375            .with_genesis_hash(genesis_hash)
1376            .map_err(de::Error::custom)?;
1377    }
1378
1379    if let Some(slow_start_interval) = slow_start_interval {
1380        params_builder = params_builder
1381            .with_slow_start_interval(slow_start_interval.try_into().map_err(de::Error::custom)?);
1382    }
1383
1384    if let Some(target_difficulty_limit) = target_difficulty_limit.clone() {
1385        params_builder = params_builder
1386            .with_target_difficulty_limit(
1387                target_difficulty_limit
1388                    .parse::<U256>()
1389                    .map_err(de::Error::custom)?,
1390            )
1391            .map_err(de::Error::custom)?;
1392    }
1393
1394    if let Some(disable_pow) = disable_pow {
1395        params_builder = params_builder.with_disable_pow(disable_pow);
1396    }
1397
1398    // Retain default Testnet activation heights unless there's an empty [testnet_parameters.activation_heights] section.
1399    if let Some(activation_heights) = activation_heights {
1400        params_builder = params_builder
1401            .with_activation_heights(activation_heights)
1402            .map_err(de::Error::custom)?
1403    }
1404
1405    if let Some(halving_interval) = pre_blossom_halving_interval {
1406        params_builder = params_builder
1407            .with_halving_interval(halving_interval.into())
1408            .map_err(de::Error::custom)?
1409    }
1410
1411    // Set configured funding streams after setting any parameters that affect the funding stream address period.
1412    let mut funding_streams_vec = funding_streams.unwrap_or_default();
1413
1414    if let Some(funding_streams) = post_nu6_funding_streams {
1415        funding_streams_vec.insert(0, funding_streams);
1416    }
1417
1418    if let Some(funding_streams) = pre_nu6_funding_streams {
1419        funding_streams_vec.insert(0, funding_streams);
1420    }
1421
1422    if !funding_streams_vec.is_empty() {
1423        params_builder = params_builder.with_funding_streams(funding_streams_vec);
1424    }
1425
1426    if let Some(lockbox_disbursements) = lockbox_disbursements {
1427        params_builder = params_builder.with_lockbox_disbursements(lockbox_disbursements);
1428    }
1429
1430    params_builder = params_builder
1431        .with_checkpoints(checkpoints)
1432        .map_err(de::Error::custom)?;
1433
1434    if let Some(true) = extend_funding_stream_addresses_as_required {
1435        params_builder = params_builder.extend_funding_streams();
1436    }
1437
1438    // Retain the default soft-fork activation height unless one is configured.
1439    if let Some(height) = temporary_orchard_disabling_soft_fork_height {
1440        params_builder = params_builder.with_temporary_orchard_disabling_soft_fork_height(
1441            height.try_into().map_err(de::Error::custom)?,
1442        );
1443    }
1444
1445    // Return an error if the initial testnet peers includes any of the default initial Mainnet or Testnet
1446    // peers and the configured network parameters are incompatible with the default public Testnet.
1447    if !params_builder.is_compatible_with_default_parameters()
1448        && contains_default_initial_peers(initial_testnet_peers)
1449    {
1450        return Err(de::Error::custom(
1451            "cannot use default initials peers with incompatible testnet",
1452        ));
1453    };
1454
1455    // Return the default Testnet if no network name was configured and all parameters match the default Testnet
1456    if network_name.is_none() && params_builder == testnet::Parameters::build() {
1457        Ok(Network::new_default_testnet())
1458    } else {
1459        Ok(params_builder.to_network().map_err(de::Error::custom)?)
1460    }
1461}
1462
1463fn build_regtest_params(params: DTestnetParameters) -> RegtestParameters {
1464    let DTestnetParameters {
1465        activation_heights,
1466        pre_nu6_funding_streams,
1467        post_nu6_funding_streams,
1468        funding_streams,
1469        lockbox_disbursements,
1470        checkpoints,
1471        extend_funding_stream_addresses_as_required,
1472        ..
1473    } = params;
1474
1475    let mut funding_streams_vec = funding_streams.unwrap_or_default();
1476
1477    if let Some(funding_streams) = post_nu6_funding_streams {
1478        funding_streams_vec.insert(0, funding_streams);
1479    }
1480
1481    if let Some(funding_streams) = pre_nu6_funding_streams {
1482        funding_streams_vec.insert(0, funding_streams);
1483    }
1484
1485    RegtestParameters {
1486        activation_heights: activation_heights.unwrap_or_default(),
1487        funding_streams: Some(funding_streams_vec),
1488        lockbox_disbursements,
1489        checkpoints: Some(checkpoints),
1490        extend_funding_stream_addresses_as_required,
1491    }
1492}