Skip to main content

hoprd/
config.rs

1use std::{collections::HashSet, net::SocketAddr, time::Duration};
2
3use hopr_lib::{
4    HoprBalance, HoprProtocolConfig, SafeModule, WinningProbability,
5    config::{
6        HoprLibConfig, HoprPacketPipelineConfig, HostConfig, HostType, ProbeConfig, SessionGlobalConfig,
7        TransportConfig,
8    },
9    exports::transport::config::HoprCodecConfig,
10};
11use hoprd_api::config::{Api, Auth};
12use proc_macro_regex::regex;
13use serde::{Deserialize, Serialize};
14use serde_with::serde_as;
15use validator::{Validate, ValidationError, ValidationErrors};
16
17pub const DEFAULT_HOST: &str = "0.0.0.0";
18pub const DEFAULT_PORT: u16 = 9091;
19
20// Validate that the path is a valid UTF-8 path.
21//
22// Also used to perform the identity file existence check on the
23// specified path, which is now circumvented but could
24// return in the future workflows of setting up a node.
25fn validate_file_path(_s: &str) -> Result<(), ValidationError> {
26    Ok(())
27
28    // if std::path::Path::new(_s).is_file() {
29    //     Ok(())
30    // } else {
31    //     Err(ValidationError::new(
32    //         "Invalid file path specified, the file does not exist or is not a file",
33    //     ))
34    // }
35}
36
37fn validate_password(s: &str) -> Result<(), ValidationError> {
38    if !s.is_empty() {
39        Ok(())
40    } else {
41        Err(ValidationError::new("No password could be found"))
42    }
43}
44
45regex!(is_private_key "^(0[xX])?[a-fA-F0-9]{128}$");
46
47pub(crate) fn validate_private_key(s: &str) -> Result<(), ValidationError> {
48    if is_private_key(s) {
49        Ok(())
50    } else {
51        Err(ValidationError::new("No valid private key could be found"))
52    }
53}
54
55fn validate_optional_private_key(s: &str) -> Result<(), ValidationError> {
56    validate_private_key(s)
57}
58
59#[derive(Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
60#[serde(deny_unknown_fields)]
61pub struct Identity {
62    #[validate(custom(function = "validate_file_path"))]
63    #[serde(default)]
64    pub file: String,
65    #[validate(custom(function = "validate_password"))]
66    #[serde(default)]
67    pub password: String,
68    #[validate(custom(function = "validate_optional_private_key"))]
69    #[serde(default)]
70    pub private_key: Option<String>,
71}
72
73impl std::fmt::Debug for Identity {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        let obfuscated: String = "<REDACTED>".into();
76
77        f.debug_struct("Identity")
78            .field("file", &self.file)
79            .field("password", &obfuscated)
80            .field("private_key", &obfuscated)
81            .finish()
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize, Validate)]
86#[serde(deny_unknown_fields)]
87pub struct Db {
88    /// Path to the directory containing the database
89    #[serde(default)]
90    pub data: String,
91    /// Determines whether the database should be initialized upon startup.
92    #[serde(default = "just_true")]
93    #[default = true]
94    pub initialize: bool,
95    /// Determines whether the database should be forcibly-initialized if it exists upon startup.
96    #[serde(default)]
97    pub force_initialize: bool,
98}
99
100fn default_session_idle_timeout() -> Duration {
101    HoprLibConfig::default().protocol.session.idle_timeout
102}
103
104fn default_max_sessions() -> usize {
105    HoprLibConfig::default().protocol.session.maximum_sessions as usize
106}
107
108fn default_session_establish_max_retries() -> usize {
109    HoprLibConfig::default().protocol.session.establish_max_retries as usize
110}
111
112fn default_probe_recheck_threshold() -> Duration {
113    HoprLibConfig::default().protocol.probe.recheck_threshold
114}
115
116fn default_probe_interval() -> Duration {
117    HoprLibConfig::default().protocol.probe.interval
118}
119
120fn default_outgoing_ticket_winning_prob() -> Option<f64> {
121    HoprLibConfig::default()
122        .protocol
123        .packet
124        .codec
125        .outgoing_win_prob
126        .map(|p| p.as_f64())
127}
128
129/// Subset of various selected HOPR library network-related configuration options.
130#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct UserHoprNetworkConfig {
133    /// How long it takes before HOPR Session is considered idle and is closed automatically
134    #[default(default_session_idle_timeout())]
135    #[serde(default = "default_session_idle_timeout", with = "humantime_serde")]
136    pub session_idle_timeout: Duration,
137    /// Maximum number of outgoing or incoming Sessions allowed by the Session manager
138    #[default(default_max_sessions())]
139    #[serde(default = "default_max_sessions")]
140    pub maximum_sessions: usize,
141    /// How many retries are made to establish an outgoing HOPR Session
142    #[default(default_session_establish_max_retries())]
143    #[serde(default = "default_session_establish_max_retries")]
144    pub session_establish_max_retries: usize,
145    /// The time interval for which to consider peer re-probing in seconds
146    #[default(default_probe_recheck_threshold())]
147    #[serde(default = "default_probe_recheck_threshold", with = "humantime_serde")]
148    pub probe_recheck_threshold: Duration,
149    /// The delay between individual probing rounds for neighbor discovery
150    #[default(default_probe_interval())]
151    #[serde(default = "default_probe_interval", with = "humantime_serde")]
152    pub probe_interval: Duration,
153    /// Should local addresses be announced on-chain?
154    #[serde(default)]
155    pub announce_local_addresses: bool,
156    /// Should local addresses be preferred when dialing a peer?
157    #[serde(default)]
158    pub prefer_local_addresses: bool,
159    /// Outgoing ticket winning probability.
160    #[default(default_outgoing_ticket_winning_prob())]
161    #[serde(default = "default_outgoing_ticket_winning_prob")]
162    pub outgoing_ticket_winning_prob: Option<f64>,
163    /// Minimum incoming ticket price.
164    ///
165    /// The value cannot be lower than the minimum network ticket price multiplied by the node's path position,
166    /// and will default to that value whenever it is lower.
167    #[serde(default)]
168    pub min_incoming_ticket_price: Option<HoprBalance>,
169}
170
171/// Subset of the [`HoprLibConfig`] that is tuned to be user-facing and more user-friendly.
172#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct UserHoprLibConfig {
175    /// Determines whether the node should be advertised publicly on-chain.
176    #[default(just_true())]
177    #[serde(default = "just_true")]
178    pub announce: bool,
179    /// Configuration related to host specifics
180    #[default(default_host())]
181    #[serde(default = "default_host")]
182    pub host: HostConfig,
183    /// Safe and Module configuration
184    #[serde(default)]
185    pub safe_module: SafeModule,
186    /// Various HOPR-network and transport-related configuration options.
187    #[serde(default)]
188    pub network: UserHoprNetworkConfig,
189}
190
191// NOTE: this intentionally does not validate (0.0.0.0) to force user to specify
192// their external IP.
193#[inline]
194fn default_host() -> HostConfig {
195    HostConfig {
196        address: HostType::IPv4(hopr_lib::config::DEFAULT_HOST.to_owned()),
197        port: hopr_lib::config::DEFAULT_PORT,
198    }
199}
200
201impl From<UserHoprLibConfig> for HoprLibConfig {
202    fn from(value: UserHoprLibConfig) -> Self {
203        HoprLibConfig {
204            host: value.host,
205            publish: value.announce,
206            safe_module: value.safe_module,
207            protocol: HoprProtocolConfig {
208                transport: TransportConfig {
209                    announce_local_addresses: value.network.announce_local_addresses,
210                    prefer_local_addresses: value.network.prefer_local_addresses,
211                },
212                packet: HoprPacketPipelineConfig {
213                    codec: HoprCodecConfig {
214                        outgoing_win_prob: value
215                            .network
216                            .outgoing_ticket_winning_prob
217                            .and_then(|v| WinningProbability::try_from_f64(v).ok()),
218                        min_incoming_ticket_price: value.network.min_incoming_ticket_price,
219                        ..Default::default()
220                    },
221                    ..Default::default()
222                },
223                probe: ProbeConfig {
224                    interval: value.network.probe_interval,
225                    recheck_threshold: value.network.probe_recheck_threshold,
226                    ..Default::default()
227                },
228                session: SessionGlobalConfig {
229                    idle_timeout: value.network.session_idle_timeout,
230                    maximum_sessions: value.network.maximum_sessions as u32,
231                    establish_max_retries: value.network.session_establish_max_retries as u32,
232                    ..Default::default()
233                },
234            },
235        }
236    }
237}
238
239impl Validate for UserHoprLibConfig {
240    fn validate(&self) -> Result<(), ValidationErrors> {
241        HoprLibConfig::from(self.clone()).validate()
242    }
243}
244
245/// The main configuration object of the entire node.
246///
247/// The configuration is composed of individual configurations of corresponding
248/// component configuration objects.
249///
250/// An always up-to-date config YAML example can be found in [example_cfg.yaml](https://github.com/hoprnet/hoprnet/tree/master/hoprd/hoprd/example_cfg.yaml)
251/// which is always in the root of this crate.
252#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq, smart_default::SmartDefault)]
253#[serde(deny_unknown_fields)]
254pub struct HoprdConfig {
255    /// Configuration related to hopr-lib functionality
256    #[validate(nested)]
257    #[serde(default)]
258    pub hopr: UserHoprLibConfig,
259    /// Configuration regarding the identity of the node
260    #[validate(nested)]
261    #[serde(default)]
262    pub identity: Identity,
263    /// Configuration of the underlying database engine
264    #[validate(nested)]
265    #[serde(default)]
266    pub db: Db,
267    /// Configuration relevant for the API of the node
268    #[validate(nested)]
269    #[serde(default)]
270    pub api: Api,
271    /// Configuration of the Session entry/exit node IP protocol forwarding.
272    #[validate(nested)]
273    #[serde(default)]
274    pub session_ip_forwarding: SessionIpForwardingConfig,
275    /// Blokli provider URL to connect to.
276    #[validate(url)]
277    pub blokli_url: Option<String>,
278    /// Configuration of underlying node behavior in the form strategies
279    ///
280    /// Strategies represent automatically executable behavior performed by
281    /// the node given pre-configured triggers.
282    #[validate(nested)]
283    #[serde(default = "hopr_strategy::hopr_default_strategies")]
284    #[default(hopr_strategy::hopr_default_strategies())]
285    pub strategy: hopr_strategy::StrategyConfig,
286}
287
288impl HoprdConfig {
289    pub fn as_redacted(&self) -> Self {
290        let mut ret = self.clone();
291        // redacting sensitive information
292        match ret.api.auth {
293            Auth::None => {}
294            Auth::Token(_) => ret.api.auth = Auth::Token("<REDACTED>".to_owned()),
295        }
296
297        if ret.identity.private_key.is_some() {
298            ret.identity.private_key = Some("<REDACTED>".to_owned());
299        }
300
301        "<REDACTED>".clone_into(&mut ret.identity.password);
302
303        ret
304    }
305
306    pub fn as_redacted_string(&self) -> crate::errors::Result<String> {
307        let redacted_cfg = self.as_redacted();
308        serde_json::to_string(&redacted_cfg).map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))
309    }
310}
311
312fn default_target_retry_delay() -> Duration {
313    Duration::from_secs(2)
314}
315
316fn default_entry_listen_host() -> SocketAddr {
317    "127.0.0.1:0".parse().unwrap()
318}
319
320fn default_max_tcp_target_retries() -> u32 {
321    10
322}
323
324fn just_true() -> bool {
325    true
326}
327
328/// Configuration of the Exit node (see [`HoprServerIpForwardingReactor`](crate::exit::HoprServerIpForwardingReactor))
329/// and the Entry node.
330#[serde_as]
331#[derive(
332    Clone, Debug, Eq, PartialEq, smart_default::SmartDefault, serde::Deserialize, serde::Serialize, validator::Validate,
333)]
334pub struct SessionIpForwardingConfig {
335    /// Controls whether allowlisting should be done via `target_allow_list`.
336    /// If set to `false`, the node will act as an Exit node for any target.
337    ///
338    /// Defaults to `true`.
339    #[serde(default = "just_true")]
340    #[default(true)]
341    pub use_target_allow_list: bool,
342
343    /// Enforces only the given target addresses (after DNS resolution).
344    ///
345    /// This is used only if `use_target_allow_list` is set to `true`.
346    /// If left empty (and `use_target_allow_list` is `true`), the node will not act as an Exit node.
347    ///
348    /// Defaults to empty.
349    #[serde(default)]
350    #[serde_as(as = "HashSet<serde_with::DisplayFromStr>")]
351    pub target_allow_list: HashSet<SocketAddr>,
352
353    /// Delay between retries in seconds to reach a TCP target.
354    ///
355    /// Defaults to 2 seconds.
356    #[serde(default = "default_target_retry_delay")]
357    #[default(default_target_retry_delay())]
358    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
359    pub tcp_target_retry_delay: Duration,
360
361    /// Maximum number of retries to reach a TCP target before giving up.
362    ///
363    /// Default is 10.
364    #[serde(default = "default_max_tcp_target_retries")]
365    #[default(default_max_tcp_target_retries())]
366    #[validate(range(min = 1))]
367    pub max_tcp_target_retries: u32,
368
369    /// Specifies the default `listen_host` for Session listening sockets
370    /// at an Entry node.
371    #[serde(default = "default_entry_listen_host")]
372    #[default(default_entry_listen_host())]
373    #[serde_as(as = "serde_with::DisplayFromStr")]
374    pub default_entry_listen_host: SocketAddr,
375}
376
377#[cfg(test)]
378mod tests {
379    use std::{
380        io::{Read, Write},
381        str::FromStr,
382    };
383
384    use anyhow::Context;
385    use clap::{Args, Command, FromArgMatches};
386    use hopr_lib::Address;
387    use tempfile::NamedTempFile;
388
389    use super::*;
390
391    pub fn example_cfg() -> anyhow::Result<HoprdConfig> {
392        let safe_module = hopr_lib::config::SafeModule {
393            safe_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
394            module_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
395        };
396
397        let identity = Identity {
398            file: "path/to/identity.file".to_string(),
399            password: "change_me".to_owned(),
400            private_key: None,
401        };
402
403        let host = HostConfig {
404            address: HostType::IPv4("1.2.3.4".into()),
405            port: 9091,
406        };
407
408        Ok(HoprdConfig {
409            hopr: UserHoprLibConfig {
410                host,
411                safe_module,
412                ..Default::default()
413            },
414            db: Db {
415                data: "/app/db".to_owned(),
416                ..Default::default()
417            },
418            identity,
419            ..HoprdConfig::default()
420        })
421    }
422
423    #[test]
424    fn test_config_should_be_serializable_into_string() -> anyhow::Result<()> {
425        let cfg = example_cfg()?;
426
427        let from_yaml: HoprdConfig = serde_saphyr::from_str(include_str!("../example_cfg.yaml"))?;
428        assert_eq!(cfg, from_yaml);
429
430        Ok(())
431    }
432
433    #[test]
434    fn test_config_should_be_deserializable_from_a_string_in_a_file() -> anyhow::Result<()> {
435        let mut config_file = NamedTempFile::new()?;
436        let mut prepared_config_file = config_file.reopen()?;
437
438        let cfg = example_cfg()?;
439        let yaml = serde_saphyr::to_string(&cfg)?;
440        config_file.write_all(yaml.as_bytes())?;
441
442        let mut buf = String::new();
443        prepared_config_file.read_to_string(&mut buf)?;
444        let deserialized_cfg: HoprdConfig = serde_saphyr::from_str(&buf)?;
445
446        assert_eq!(deserialized_cfg, cfg);
447
448        Ok(())
449    }
450
451    /// TODO: This test attempts to deserialize the data structure incorrectly in the native build
452    /// (`confirmations`` are an extra field), as well as misses the native implementation for the
453    /// version satisfies check
454    #[test]
455    #[ignore]
456    fn test_config_is_extractable_from_the_cli_arguments() -> anyhow::Result<()> {
457        let pwnd = "rpc://pawned!";
458
459        let mut config_file = NamedTempFile::new()?;
460
461        let mut cfg = example_cfg()?;
462        cfg.blokli_url = Some(pwnd.to_owned());
463
464        let yaml = serde_saphyr::to_string(&cfg)?;
465        config_file.write_all(yaml.as_bytes())?;
466        let cfg_file_path = config_file
467            .path()
468            .to_str()
469            .context("file path should have a string representation")?
470            .to_string();
471
472        let cli_args = vec!["hoprd", "--configurationFilePath", cfg_file_path.as_str()];
473
474        let mut cmd = Command::new("hoprd").version("0.0.0");
475        cmd = crate::cli::CliArgs::augment_args(cmd);
476        let derived_matches = cmd.try_get_matches_from(cli_args)?;
477        let args = crate::cli::CliArgs::from_arg_matches(&derived_matches)?;
478
479        // skipping validation
480        let cfg = HoprdConfig::try_from(args)?;
481
482        assert_eq!(cfg.blokli_url, Some(pwnd.to_owned()));
483
484        Ok(())
485    }
486}