Skip to main content

zebrad/components/zcashd_compat/
config.rs

1use std::{
2    net::{IpAddr, SocketAddr},
3    path::PathBuf,
4    time::Duration,
5};
6
7use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
8
9/// Source selector for supervised `zcashd` execution.
10#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize, Default)]
11#[serde(rename_all = "snake_case")]
12pub enum ZcashdBinarySource {
13    /// Resolve `zcashd` from a local executable path.
14    #[default]
15    Path,
16    /// Resolve `zcashd` from Zebra's embedded release manifest.
17    Embedded,
18}
19
20/// Configuration for Zebra zcashd-compat mode.
21#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
22#[serde(deny_unknown_fields, default)]
23pub struct Config {
24    /// Enables zcashd-compat mode.
25    ///
26    /// zcashd-compat mode supervises or validates a P2P sidecar `zcashd` process
27    /// that syncs chain data from Zebra over the legacy Zcash P2P protocol.
28    pub enabled: bool,
29
30    /// Whether Zebra should spawn and supervise a `zcashd -zebra-compat` child process.
31    ///
32    /// Set this to `false` if `zcashd` is managed externally.
33    pub manage_zcashd: bool,
34
35    /// Preferred source for the `zcashd` binary.
36    ///
37    /// If `zcashd_path` is set, that explicit local path overrides this value.
38    pub zcashd_source: ZcashdBinarySource,
39
40    /// Optional explicit path to a local `zcashd` binary with zcashd-compat support.
41    ///
42    /// When set, Zebra uses this path directly and skips embedded downloads.
43    pub zcashd_path: Option<PathBuf>,
44
45    /// Optional `zcashd` datadir path.
46    ///
47    /// If unset, Zebra uses a subdirectory in `state.cache_dir`.
48    pub zcashd_datadir: Option<PathBuf>,
49
50    /// Extra command-line arguments passed to `zcashd`.
51    ///
52    /// This can be provided as:
53    /// - a TOML array: `zcashd_extra_args = ["-debug=1"]`
54    /// - a JSON array string (useful for environment variable overrides):
55    ///   `ZEBRA_ZCASHD_COMPAT__ZCASHD_EXTRA_ARGS='["-conf=/path/to/zcash.conf","-debug=1"]'`
56    ///
57    /// Zebra passes these arguments through unchanged. For first-start bootstrap,
58    /// Zebra only infers path overrides from the first valid `-conf=/path` or
59    /// `-datadir=/path` form, and logs warnings for paired, empty, or duplicate
60    /// path options.
61    ///
62    /// Supervised zcashd runs always include `-printtoconsole` automatically.
63    #[serde(default, deserialize_with = "deserialize_zcashd_extra_args")]
64    pub zcashd_extra_args: Vec<String>,
65
66    /// The Zebra legacy P2P address supervised zcashd connects to via `-connect`.
67    ///
68    /// If unset, Zebra derives it from its own bound legacy P2P listener
69    /// (`network.listen_addr`), substituting `127.0.0.1` when the listener is
70    /// bound to an unspecified address. Set this only when zcashd must reach
71    /// Zebra through a different address, such as across containers.
72    pub p2p_connect_addr: Option<SocketAddr>,
73
74    /// Inbound sidecar peer IPs that must always receive block inventory broadcasts.
75    ///
76    /// If empty while zcashd-compat is enabled, Zebra defaults this list to
77    /// loopback addresses. This setting is rejected when zcashd-compat is
78    /// disabled.
79    ///
80    /// # Security
81    ///
82    /// This is a trusted-peer privilege boundary, not just a gossip list.
83    /// Inbound peers connecting from these IPs also:
84    /// - use a reserved inbound connection pool of one slot per listed IP
85    ///   (shared across the list, without per-IP accounting), reducing the
86    ///   public inbound pool by the same amount; when the reserved pool is
87    ///   full, further connections from listed IPs compete for public slots
88    ///   like any other peer,
89    /// - bypass the recent-IP reconnection rate limit while a reserved slot
90    ///   is free (fallback connections are rate limited normally), and
91    /// - are exempt from the `FindBlocks`/`FindHeaders` sync stall detector.
92    ///
93    /// Only list addresses where every process is trusted, like the loopback
94    /// addresses of a host that runs nothing but Zebra and its sidecar.
95    /// Never list IPs that untrusted machines can connect from.
96    ///
97    /// Because environment values cannot express TOML arrays, this also accepts
98    /// a JSON array string, e.g.
99    /// `ZEBRA_ZCASHD_COMPAT__BLOCK_GOSSIP_PEER_IPS='["10.0.0.5"]'`.
100    #[serde(default, deserialize_with = "deserialize_block_gossip_peer_ips")]
101    pub block_gossip_peer_ips: Vec<IpAddr>,
102
103    /// Delay before the first `zcashd` spawn attempt.
104    #[serde(with = "humantime_serde")]
105    pub startup_delay: Duration,
106
107    /// Delay between supervisor restart attempts.
108    ///
109    /// This is the base delay for exponential restart backoff.
110    #[serde(with = "humantime_serde")]
111    pub restart_backoff: Duration,
112
113    /// Maximum delay between supervisor restart attempts.
114    ///
115    /// This caps exponential restart backoff while retries continue indefinitely.
116    #[serde(with = "humantime_serde")]
117    pub restart_backoff_max: Duration,
118
119    /// Child uptime that resets the supervisor's consecutive restart count.
120    #[serde(with = "humantime_serde")]
121    pub restart_reset_after: Duration,
122
123    /// Grace period for a clean shutdown after sending SIGTERM.
124    #[serde(with = "humantime_serde")]
125    pub shutdown_grace_period: Duration,
126}
127
128impl Default for Config {
129    /// Returns conservative zcashd-compat defaults for an externally managed zcashd.
130    ///
131    /// Defaults keep zcashd-compat disabled unless explicitly requested, and do
132    /// not spawn `zcashd` unless supervision is explicitly enabled. The
133    /// restart/shutdown settings apply only to supervised `zcashd` children.
134    fn default() -> Self {
135        Self {
136            enabled: false,
137            manage_zcashd: false,
138            zcashd_source: ZcashdBinarySource::Path,
139            zcashd_path: None,
140            zcashd_datadir: None,
141            zcashd_extra_args: Vec::new(),
142            p2p_connect_addr: None,
143            block_gossip_peer_ips: Vec::new(),
144            startup_delay: Duration::from_secs(1),
145            restart_backoff: Duration::from_secs(2),
146            restart_backoff_max: Duration::from_secs(5 * 60),
147            restart_reset_after: Duration::from_secs(60 * 60),
148            shutdown_grace_period: Duration::from_secs(300),
149        }
150    }
151}
152
153/// Deserializes `zcashd_extra_args` from either a sequence or a JSON-array string.
154fn deserialize_zcashd_extra_args<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
155where
156    D: Deserializer<'de>,
157{
158    #[derive(Deserialize)]
159    #[serde(untagged)]
160    enum ExtraArgsField {
161        Sequence(Vec<String>),
162        JsonString(String),
163    }
164
165    match ExtraArgsField::deserialize(deserializer)? {
166        ExtraArgsField::Sequence(args) => Ok(args),
167        ExtraArgsField::JsonString(args) => {
168            serde_json::from_str(&args).map_err(|error| {
169                D::Error::custom(format!(
170                    "zcashd_extra_args must be a sequence or a JSON string array, got: {args:?}. parse error: {error}"
171                ))
172            })
173        }
174    }
175}
176
177/// Deserializes `block_gossip_peer_ips` from either a sequence or a JSON-array
178/// string, so it can be set through the environment (which cannot express TOML
179/// arrays) in cross-container deployments where the sidecar's source IP is not
180/// loopback.
181fn deserialize_block_gossip_peer_ips<'de, D>(deserializer: D) -> Result<Vec<IpAddr>, D::Error>
182where
183    D: Deserializer<'de>,
184{
185    #[derive(Deserialize)]
186    #[serde(untagged)]
187    enum PeerIpsField {
188        Sequence(Vec<IpAddr>),
189        JsonString(String),
190    }
191
192    match PeerIpsField::deserialize(deserializer)? {
193        PeerIpsField::Sequence(ips) => Ok(ips),
194        PeerIpsField::JsonString(ips) => serde_json::from_str(&ips).map_err(|error| {
195            D::Error::custom(format!(
196                "block_gossip_peer_ips must be a sequence or a JSON string array, got: {ips:?}. parse error: {error}"
197            ))
198        }),
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use std::net::{IpAddr, Ipv4Addr};
205
206    use super::{Config, ZcashdBinarySource};
207
208    #[test]
209    fn defaults_to_unsupervised_path_source_without_explicit_path() {
210        let config = Config::default();
211        assert!(!config.manage_zcashd);
212        assert_eq!(config.zcashd_source, ZcashdBinarySource::Path);
213        assert_eq!(config.zcashd_path, None);
214        assert_eq!(
215            config.restart_reset_after,
216            std::time::Duration::from_secs(60 * 60)
217        );
218        assert_eq!(
219            config.restart_backoff_max,
220            std::time::Duration::from_secs(5 * 60)
221        );
222    }
223
224    #[test]
225    fn default_shutdown_grace_period_allows_zcashd_to_flush_state() {
226        let config = Config::default();
227
228        assert_eq!(
229            config.shutdown_grace_period,
230            std::time::Duration::from_secs(300)
231        );
232    }
233
234    #[test]
235    fn deserialize_restart_reset_after_duration() {
236        let config: Config = toml::from_str(
237            r#"
238            restart_reset_after = "30m"
239            "#,
240        )
241        .expect("restart reset duration should deserialize");
242
243        assert_eq!(
244            config.restart_reset_after,
245            std::time::Duration::from_secs(30 * 60)
246        );
247    }
248
249    #[test]
250    fn deserialize_restart_backoff_max_duration() {
251        let config: Config = toml::from_str(
252            r#"
253            restart_backoff_max = "10m"
254            "#,
255        )
256        .expect("restart backoff cap duration should deserialize");
257
258        assert_eq!(
259            config.restart_backoff_max,
260            std::time::Duration::from_secs(10 * 60)
261        );
262    }
263
264    #[test]
265    fn deserialize_block_gossip_peer_ips() {
266        let config: Config = toml::from_str(
267            r#"
268            block_gossip_peer_ips = ["127.0.0.1"]
269            "#,
270        )
271        .expect("sidecar block gossip peer IPs should deserialize");
272
273        assert_eq!(
274            config.block_gossip_peer_ips,
275            vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]
276        );
277    }
278
279    #[test]
280    fn deserialize_extra_args_from_sequence() {
281        let config: Config = toml::from_str(
282            r#"
283            zcashd_extra_args = ["-conf=/tmp/zcash.conf", "-debug=1"]
284            "#,
285        )
286        .expect("valid sequence should deserialize");
287
288        assert_eq!(
289            config.zcashd_extra_args,
290            vec!["-conf=/tmp/zcash.conf".to_string(), "-debug=1".to_string()]
291        );
292    }
293
294    #[test]
295    fn deserialize_extra_args_from_json_string() {
296        let config: Config = toml::from_str(
297            r#"
298            zcashd_extra_args = "[\"-conf=/tmp/zcash.conf\",\"-debug=1\"]"
299            "#,
300        )
301        .expect("valid JSON string array should deserialize");
302
303        assert_eq!(
304            config.zcashd_extra_args,
305            vec!["-conf=/tmp/zcash.conf".to_string(), "-debug=1".to_string()]
306        );
307    }
308
309    #[test]
310    fn reject_non_array_string_extra_args() {
311        let error = toml::from_str::<Config>(
312            r#"
313            zcashd_extra_args = "-debug=1"
314            "#,
315        )
316        .expect_err("plain strings should be rejected");
317
318        let error_message = error.to_string();
319        assert!(
320            error_message.contains("JSON string array"),
321            "error should explain expected format: {error_message}"
322        );
323    }
324}