delta_domain_sdk/
config.rs

1//! # Config
2//!
3//! Domain configuration, loaded from a file as [`ConfigFile`] and resolved into
4//! the in-memory form [`Config`] used at runtime.
5
6use base_sdk::{
7    core::Shard,
8    crypto::{
9        ed25519,
10        read_keypair,
11    },
12};
13use serde::{
14    Deserialize,
15    Serialize,
16};
17use snafu::{
18    OptionExt,
19    ResultExt,
20    Snafu,
21};
22use std::{
23    num::NonZero,
24    path::{
25        Path,
26        PathBuf,
27    },
28};
29
30/// Error occurring when reading domain config
31#[derive(Debug, Snafu)]
32pub enum Error {
33    /// Error from config-rs
34    #[snafu(display("Could not read config: {source}"))]
35    ReadConfig {
36        /// The underlying error
37        source: config::ConfigError,
38    },
39
40    /// Read key error
41    #[snafu(display("Could not read keypair: {source}"))]
42    ReadKey {
43        /// The underlying error
44        source: base_sdk::crypto::IoError,
45    },
46
47    /// Shard 0 error
48    #[snafu(display("Shard 0 is reserved and cannot be a domain"))]
49    ShardZeroReserved,
50    /// Error getting current directory
51    #[snafu(display("Error getting current directory: check permission or existence"))]
52    BadCurrentDirectory,
53}
54
55/// Contents of the domain configuration file
56///
57/// Configuration files can be written in one of the [allowed
58/// formats](config::FileFormat) and are read with [Config::load].
59#[derive(Debug, Deserialize, Serialize, Clone)]
60pub struct ConfigFile {
61    /// Shard number this domain operates on
62    pub shard: Shard,
63    /// Path to a JSON file containing the (ed25519) keypair the domain signs
64    /// transactions with.
65    ///
66    /// This [PathBuf] value, when indicating a relative path, is relative to
67    /// the location of the configuration file.
68    pub keypair: PathBuf,
69    /// URL of the base layer RPC to connect to.
70    /// If none, a mock RPC is used.
71    pub rpc_url: Option<String>,
72    /// Storage path. This is ignored when not using the rocksdb feature
73    pub storage: Option<StorageConfig>,
74}
75
76/// Storage configuration
77#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
78pub enum StorageConfig {
79    /// RocksDb configuration
80    #[serde(rename = "rocksdb")]
81    RocksDb {
82        /// Storage path root
83        path: PathBuf,
84    },
85    /// In memory configuration
86    #[serde(rename = "in_memory")]
87    InMemory,
88}
89
90/// # Domain configuration
91///
92/// By default a `domain.yaml` file is [loaded](Config::load) to configure a domain.
93/// The file has the following structure:
94///
95/// ```yaml
96#[doc = include_str!("../doc/domain.yaml")]
97/// ```
98/// 
99/// ## Dev Note
100///
101/// This is the in-memory, resolved equivalent of [ConfigFile]. In comparison
102/// with [ConfigFile], all files/keys/paths are resolved and read into memory.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct Config {
105    /// the shard this domain operates
106    pub shard: NonZero<Shard>,
107    /// Keypair that identifies the domain operator and with which it signs
108    /// transactions
109    pub keypair: ed25519::PrivKey,
110    /// URL of the base layer RPC to connect to.
111    /// If none, a mock RPC is used.
112    pub rpc_url: Option<String>,
113    /// Storage configuration
114    pub storage: Option<StorageConfig>,
115}
116
117impl Config {
118    /// Load [Config] from a file called `domain` in the current directory.
119    ///
120    /// Environment variables prefixed with `DOMAIN_` can override
121    /// configuration in the file. The file can be in any [allowed
122    /// format](config::FileFormat) and has to have the contents of
123    /// [ConfigFile].
124    pub fn load() -> Result<Self, Error> {
125        let current_directory = std::env::current_dir().map_err(|_| Error::BadCurrentDirectory)?;
126        Self::read(config::File::with_name("domain"), current_directory)
127    }
128
129    /// Load [Config] from a configuration file at the given `path`.
130    ///
131    /// Environment variables prefixed with `DOMAIN_` can override
132    /// configuration in the file. The file can be in any [allowed
133    /// format](config::FileFormat) and has to have the contents of
134    /// [ConfigFile].
135    pub fn load_from(path: impl AsRef<Path>) -> Result<Self, Error> {
136        let path = path.as_ref();
137        let config_dir = path
138            .parent()
139            .map(|p| p.to_path_buf())
140            .unwrap_or_else(|| PathBuf::from("."));
141        Self::read(config::File::from(path), config_dir)
142    }
143
144    fn read<S>(src: S, config_dir: PathBuf) -> Result<Self, Error>
145    where
146        S: config::Source + Send + Sync + 'static,
147    {
148        let ConfigFile {
149            shard,
150            keypair,
151            rpc_url,
152            storage: storage_path,
153        } = config::Config::builder()
154            .add_source(src)
155            .add_source(config::Environment::with_prefix("DOMAIN"))
156            .build()
157            .context(ReadConfigSnafu)?
158            .try_deserialize()
159            .context(ReadConfigSnafu)?;
160
161        let shard = NonZero::new(shard).context(ShardZeroReservedSnafu)?;
162
163        // Resolve keypair path relative to config file directory if it's a relative path
164        let keypair_path = if keypair.is_relative() {
165            config_dir.join(&keypair)
166        } else {
167            keypair
168        };
169        tracing::debug!("Reading keypair from {}", keypair_path.display());
170
171        let keypair = read_keypair(keypair_path).context(ReadKeySnafu)?;
172        tracing::info!("Using public key: {}", keypair.pub_key());
173
174        Ok(Self {
175            shard,
176            keypair,
177            rpc_url,
178            storage: storage_path,
179        })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use serial_test::{
187        parallel,
188        serial,
189    };
190    use serializers::json;
191
192    fn setup_path(prefix: impl AsRef<Path>) -> (PathBuf, ed25519::PrivKey) {
193        let dir = std::env::temp_dir().join(prefix);
194        std::fs::create_dir_all(&dir).unwrap();
195        let keypair = ed25519::PrivKey::generate();
196        let keypath = dir.join("key.json");
197        json::write_path(keypath, &keypair).unwrap();
198        (dir, keypair)
199    }
200
201    #[test]
202    #[parallel]
203    fn test_from_yaml() {
204        let (path, keypair) = setup_path("yml");
205        let yaml_path = path.join("domain.yaml");
206
207        let yaml = format!(
208            r#"
209                shard: 123
210                keypair: {}/key.json
211                rpc_url: http://localhost:9000
212                storage:
213                  rocksdb:
214                    path: /tmp/db
215            "#,
216            path.display()
217        );
218        std::fs::write(&yaml_path, yaml).unwrap();
219
220        assert_eq!(
221            Config::load_from(&yaml_path).unwrap(),
222            Config {
223                shard: NonZero::new(123).unwrap(),
224                keypair: keypair.clone(),
225                rpc_url: Some("http://localhost:9000".to_string()),
226                storage: Some(StorageConfig::RocksDb {
227                    path: "/tmp/db".into()
228                })
229            }
230        );
231
232        let yaml = format!(
233            r#"
234                shard: 123
235                keypair: {}/key.json
236            "#,
237            path.display()
238        );
239        std::fs::write(&yaml_path, yaml).unwrap();
240
241        assert_eq!(
242            Config::load_from(&yaml_path).unwrap(),
243            Config {
244                shard: NonZero::new(123).unwrap(),
245                keypair,
246                rpc_url: None,
247                storage: None,
248            }
249        );
250
251        let yaml = format!(
252            r#"
253                shard: hello
254                keypair: {}/key.json
255            "#,
256            path.display()
257        );
258        std::fs::write(&yaml_path, yaml).unwrap();
259        assert!(Config::load_from(&yaml_path).is_err());
260
261        let yaml = r#"
262            shard: 1
263            keypair: ../wrong/path/key.json
264        "#;
265        std::fs::write(&yaml_path, yaml).unwrap();
266        assert!(Config::load_from(&yaml_path).is_err());
267
268        let yaml = format!("keypair: {}/key.json", path.display());
269        std::fs::write(&yaml_path, yaml).unwrap();
270        assert!(Config::load_from(yaml_path).is_err());
271    }
272
273    #[test]
274    #[parallel]
275    fn test_from_toml() {
276        let (path, keypair) = setup_path("toml");
277        let toml = format!(
278            r#"
279                shard = 123
280                keypair = "{}/key.json"
281            "#,
282            path.display()
283        );
284        let path = path.join("domain.toml");
285        std::fs::write(&path, toml).unwrap();
286
287        assert_eq!(
288            Config::load_from(path).unwrap(),
289            Config {
290                shard: NonZero::new(123).unwrap(),
291                keypair,
292                rpc_url: None,
293                storage: None,
294            }
295        );
296    }
297
298    #[test]
299    #[parallel]
300    fn test_from_json() {
301        let (path, keypair) = setup_path("json");
302        let json = format!(
303            r#"{{
304                "shard": "123",
305                "keypair": "{}/key.json"
306            }}"#,
307            path.display()
308        );
309        let path = path.join("domain.json");
310        std::fs::write(&path, json).unwrap();
311
312        assert_eq!(
313            Config::load_from(path).unwrap(),
314            Config {
315                shard: NonZero::new(123).unwrap(),
316                keypair,
317                rpc_url: None,
318                storage: None,
319            }
320        );
321    }
322
323    #[test]
324    #[parallel]
325    fn test_relative_from_json() {
326        // This path needs to be different from other paths for tests to be run
327        // in parallel
328        let (path, keypair) = setup_path("json_rel");
329
330        let json = r#"{
331                "shard": "123",
332                "keypair": "key.json"
333            }"#
334        .to_string();
335        let path1 = path.join("domain_relative_straight.json");
336        std::fs::write(&path1, json).unwrap();
337
338        assert_eq!(
339            Config::load_from(path1).unwrap(),
340            Config {
341                shard: NonZero::new(123).unwrap(),
342                keypair: keypair.clone(),
343                rpc_url: None,
344                storage: None,
345            }
346        );
347
348        let json = r#"{
349                "shard": "123",
350                "keypair": "./key.json"
351            }"#
352        .to_string();
353        let path2 = path.join("domain_relative_dot.json");
354        std::fs::write(&path2, json).unwrap();
355
356        assert_eq!(
357            Config::load_from(path2).unwrap(),
358            Config {
359                shard: NonZero::new(123).unwrap(),
360                keypair,
361                rpc_url: None,
362                storage: None,
363            }
364        );
365
366        // Make sure constructed path has enough depths for `components` below to provide a value.
367        // If `setup_path` changes, this could be a source of issue.
368        let (path, keypair) = setup_path("some/json/subfolder");
369
370        // Assume everything goes well here. If `to_str` does not work, let's
371        // have the test fail anyway, this is worth looking into.
372        let dirname = path
373            .components()
374            .next_back()
375            // This should not fail due to the aforementioned path construction
376            .unwrap()
377            .as_os_str()
378            .to_str()
379            .unwrap();
380
381        let json = format!(
382            r#"{{
383                "shard": "123",
384                "keypair": "../{dirname}/key.json"
385            }}"#
386        );
387        let path = path.join("domain_relative_dotdot.json");
388        std::fs::write(&path, json).unwrap();
389
390        assert_eq!(
391            Config::load_from(path).unwrap(),
392            Config {
393                shard: NonZero::new(123).unwrap(),
394                keypair,
395                rpc_url: None,
396                storage: None,
397            }
398        );
399    }
400
401    #[test]
402    #[serial]
403    fn test_env_override() {
404        let (path, keypair) = setup_path("env");
405        let json = format!(
406            r#"{{
407                "keypair": "{}/key.json",
408                "rpc_url": "https://awesome.net/rpc"
409            }}"#,
410            path.display()
411        );
412        let path = path.join("domain.json");
413        std::fs::write(&path, json).unwrap();
414
415        // override config with env vars
416        std::env::set_var("DOMAIN_SHARD", "666");
417        std::env::set_var("DOMAIN_RPC_URL", "http://boring.but/real");
418
419        assert_eq!(
420            Config::load_from(path).unwrap(),
421            Config {
422                shard: NonZero::new(666).unwrap(),
423                keypair,
424                rpc_url: Some("http://boring.but/real".to_string()),
425                storage: None,
426            }
427        );
428    }
429}