delta_domain_sdk/
builder.rs

1//! # Builder
2//!
3//! Builder API to configure and construct new domains.
4//!
5//! See [`DomainBuilder`] and [`Domain`] for creating a configured domain
6//! instance.
7
8#[cfg(feature = "rocksdb")]
9use crate::storage::options::DbOptions;
10use crate::{
11    base_layer::{
12        mock,
13        rpc,
14        BaseLayer,
15    },
16    config::{
17        Config,
18        StorageConfig,
19    },
20    domain_agreement,
21    in_memory,
22    proving,
23    storage::{
24        Database,
25        DefaultStorage,
26    },
27    Domain,
28};
29use base_sdk::{
30    core::Shard,
31    crypto::ed25519,
32    vaults::{
33        Address,
34        Vault,
35    },
36};
37use domain_runtime::{
38    config::{
39        BaseLayerRetryConfig,
40        RuntimeConfig,
41        SdlRetentionConfig,
42        SignatureVerificationConfig,
43    },
44    storage::ColumnFamilies,
45};
46use http::{
47    uri::InvalidUri,
48    Uri,
49};
50use proving_clients::Proving;
51use snafu::{
52    ResultExt,
53    Snafu,
54};
55use std::{
56    collections::HashMap,
57    net::{
58        Ipv6Addr,
59        SocketAddr,
60    },
61    num::NonZero,
62};
63use storage::{
64    column_family::ColumnFamilies as _,
65    database::{
66        GenericDatabase,
67        StorageError,
68    },
69    key_value::KeyValueStorage,
70};
71
72/// Errors occurring in the [DomainBuilder]
73#[derive(Debug, Snafu)]
74pub enum Error {
75    /// Base layer client initialization failed
76    #[snafu(display("Base layer error: {source}"))]
77    BaseLayer {
78        /// The underlying error from the base layer client
79        source: Box<dyn std::error::Error + Send + Sync>,
80    },
81    /// Error when reading from storage
82    #[snafu(display("Error while initializing storage: {source}"))]
83    Storage {
84        /// The underlying storage error
85        source: StorageError,
86    },
87}
88
89/// Default address to expose the admin API if the feature is active.
90pub const ADMIN_API_DEFAULT_ADDR: SocketAddr =
91    SocketAddr::new(std::net::IpAddr::V6(Ipv6Addr::UNSPECIFIED), 4001);
92
93/// Builder to configure and create new domains
94///
95/// Obtain an instance via [Domain::builder] or [Domain::from_config], then
96/// configure the domain with, for example:
97/// - [`with_rpc`](Self::with_rpc) to connect to a base layer RPC
98/// - [`with_proving_client`](Self::with_proving_client) to set the proving
99///   client
100///
101/// Finally, call [`build`](Self::build) to create the [Domain].
102#[derive(Debug)]
103pub struct DomainBuilder<S> {
104    config: RuntimeConfig,
105    keypair: ed25519::PrivKey,
106    base_layer: BaseLayerOptions,
107    proving: Box<dyn Proving>,
108    storage: S,
109    admin_api_address: SocketAddr,
110}
111
112/// Options to configure the RPC client
113#[derive(Debug)]
114enum BaseLayerOptions {
115    Rpc { url: Result<Uri, Error> },
116    Mock { vaults: HashMap<Address, Vault> },
117}
118
119impl BaseLayerOptions {
120    fn mock() -> Self {
121        Self::Mock {
122            vaults: Default::default(),
123        }
124    }
125}
126
127impl Domain<DefaultStorage> {
128    /// Construct a new domain from configuration.
129    ///
130    /// See [Config] for how to load configuration.
131    ///
132    /// By default, a mock RPC is configured. Use [DomainBuilder::with_rpc] or
133    /// set a `rpc_url` in the configuration file to use a real base layer RPC.
134    ///
135    /// By default, a mock proving client is used. To configure a proving client
136    /// use [`with_proving_client`](DomainBuilder::with_proving_client) or set
137    /// `proving` in the configuration file.
138    ///
139    /// ## Returns
140    /// A [DomainBuilder] to continue configuration or build the domain.
141    pub fn from_config(config: Config) -> DomainBuilder<DbBuilderType> {
142        let builder = Self::builder(config.shard, config.keypair);
143
144        let builder = if let Some(retention) = config.sdl_retention {
145            builder.with_sdl_retention(retention)
146        } else {
147            builder
148        };
149
150        let builder = if let Some(url) = config.rpc_url {
151            builder.with_rpc(url)
152        } else {
153            builder
154        };
155
156        #[cfg(not(feature = "rocksdb"))]
157        if let Some(StorageConfig::Rocksdb { .. }) = config.storage {
158            panic!("The `rocksdb` feature is not enabled, but the configuration requests it.");
159        }
160
161        #[cfg(feature = "rocksdb")]
162        let builder = match config.storage {
163            Some(StorageConfig::InMemory) => {
164                panic!("The `rocksdb` feature is enabled, but the configuration requests in-memory storage.");
165            },
166            Some(StorageConfig::Rocksdb { path }) => {
167                builder.with_rocksdb(DbOptions::default().with_db_prefix_path(path))
168            },
169            None => builder,
170        };
171
172        #[cfg(not(feature = "admin-api"))]
173        if config.admin_api.is_some() {
174            panic!("The `admin-api` feature is not enabled, but the configuration requests it.");
175        }
176
177        #[cfg(feature = "admin-api")]
178        let builder = if let Some(addr) = config.admin_api {
179            builder.with_admin_api_ip(addr)
180        } else {
181            builder
182        };
183
184        if let Some(proving_config) = config.proving {
185            builder.with_proving_client(proving_config.into_proving_client())
186        } else {
187            builder
188        }
189    }
190
191    /// Construct a new domain with the builder.
192    ///
193    /// The storage layer used is defined by the cargo feature `rocksdb`
194    /// (RocksDB if set, in-memory if not).
195    ///
196    /// # Parameters
197    /// * `shard` - the shard this domain operates
198    /// * `keypair` - the domain signs transactions with
199    ///
200    /// ## Returns
201    /// A [DomainBuilder] to continue configuration or build the domain.
202    pub fn builder(
203        shard: NonZero<Shard>,
204        keypair: ed25519::PrivKey,
205    ) -> DomainBuilder<DbBuilderType> {
206        #[cfg(not(feature = "rocksdb"))]
207        let storage = Database::new(in_memory::Storage::new());
208        #[cfg(feature = "rocksdb")]
209        let storage = DbOptions::default();
210
211        // this is the unique place where default values are defined
212        DomainBuilder {
213            config: RuntimeConfig {
214                shard,
215                signature_verification: SignatureVerificationConfig::Enabled,
216                sdl_retention: SdlRetentionConfig::default(),
217                base_layer_retry: BaseLayerRetryConfig::OnEpochMismatch,
218            },
219            keypair,
220            storage,
221            base_layer: BaseLayerOptions::mock(),
222            proving: Box::new(proving::mock::Client::global_laws()),
223            admin_api_address: ADMIN_API_DEFAULT_ADDR,
224        }
225    }
226
227    /// Same as [Self::builder] but with storage forced to in-memory and
228    /// base layer retry activated.
229    ///
230    ///  NOTE: tests should always use this constructor!
231    pub fn in_mem_builder(
232        shard: NonZero<Shard>,
233        keypair: ed25519::PrivKey,
234    ) -> DomainBuilder<Database<in_memory::Storage>> {
235        Self::builder(shard, keypair).with_storage(in_memory::Storage::new())
236    }
237}
238
239impl<S> DomainBuilder<S> {
240    /// Use the given proving client to generate proofs.
241    ///
242    /// This can be a custom implementation of [Proving] or a provided one, for
243    /// example SP1 (the `sp1` feature needs to be active for that):
244    /// ```rust,no_run
245    /// use delta_domain_sdk::{proving, Domain};
246    /// # use std::num::NonZero;
247    /// # use base_sdk::crypto::ed25519::PrivKey;
248    /// # let builder = Domain::builder(NonZero::new(1).unwrap(), PrivKey::generate());
249    /// # let local_laws_elf = &[1,2,3];
250    ///
251    /// # #[cfg(feature = "sp1")]
252    /// builder.with_proving_client(
253    ///     Box::new(proving::sp1::Client::global_laws_cpu()
254    ///         .with_local_laws_cpu(local_laws_elf))
255    /// );
256    /// ```
257    pub fn with_proving_client(self, proving: Box<dyn Proving>) -> Self {
258        Self {
259            config: self.config,
260            keypair: self.keypair,
261            base_layer: self.base_layer,
262            proving,
263            storage: self.storage,
264            admin_api_address: self.admin_api_address,
265        }
266    }
267
268    /// Use a custom [KeyValueStorage] implementation
269    ///
270    /// ## Note
271    /// SDK assumes that iteration order for [column families][ColumnFamilies]
272    /// is in lexicographical order of the keys/indices.
273    pub fn with_storage<S2>(self, storage: S2) -> DomainBuilder<Database<S2>>
274    where
275        S2: KeyValueStorage<ColumnFamilyIdentifier = ColumnFamilies, Error = StorageError>
276            + Send
277            + Sync
278            + 'static,
279    {
280        self.with_database(Database::new(storage))
281    }
282
283    /// Use a custom [KeyValueStorage] implementation wrapped in a [Database].
284    ///
285    /// ## Note
286    /// SDK assumes that iteration order for [column families][ColumnFamilies]
287    /// is in lexicographical order of the keys/indices.
288    pub fn with_database<S2>(self, database: Database<S2>) -> DomainBuilder<Database<S2>>
289    where
290        S2: KeyValueStorage<ColumnFamilyIdentifier = ColumnFamilies, Error = StorageError>
291            + Send
292            + Sync
293            + 'static,
294    {
295        DomainBuilder {
296            config: self.config,
297            keypair: self.keypair,
298            base_layer: self.base_layer,
299            proving: self.proving,
300            storage: database,
301            admin_api_address: self.admin_api_address,
302        }
303    }
304
305    /// Configure SDL retention behavior.
306    pub const fn with_sdl_retention(mut self, sdl_retention: SdlRetentionConfig) -> Self {
307        self.config.sdl_retention = sdl_retention;
308        self
309    }
310
311    /// Configure base layer transaction retry.
312    pub const fn with_base_layer_retry(mut self, config: BaseLayerRetryConfig) -> Self {
313        self.config.base_layer_retry = config;
314        self
315    }
316
317    /// Use an RPC to connect to the base layer network.
318    ///
319    /// # Parameters
320    /// * `url` - of the base layer RPC
321    pub fn with_rpc(mut self, url: impl TryInto<Uri, Error = InvalidUri> + Send) -> Self {
322        self.base_layer = BaseLayerOptions::Rpc {
323            url: url.try_into().boxed().context(BaseLayerSnafu),
324        };
325        self
326    }
327    /// Override any previously configured RPC (even `rpc_url` from config file)
328    /// and use a mock RPC as base layer network that can return the given
329    /// `mock_vaults` when requested.
330    ///
331    /// Note that only sharded vaults are supported.
332    pub fn with_mock_rpc(mut self, mock_vaults: HashMap<Address, Vault>) -> Self {
333        self.base_layer = BaseLayerOptions::Mock {
334            vaults: mock_vaults,
335        };
336        self
337    }
338
339    /// Expose the admin HTTP API under the given `port` and IPv6 address `[::]`.
340    ///
341    /// Note if the`admin-api` feature is active, the API is by default exposed
342    /// under [ADMIN_API_DEFAULT_ADDR].
343    #[cfg(feature = "admin-api")]
344    pub fn with_admin_api(self, port: u16) -> Self {
345        self.with_admin_api_ip((ADMIN_API_DEFAULT_ADDR.ip(), port).into())
346    }
347
348    /// Expose the admin HTTP API under the given IP address and port.
349    ///
350    /// Note if the`admin-api` feature is active, the API is by default exposed
351    /// under IP `[::]` and [ADMIN_API_DEFAULT_ADDR].
352    #[cfg(feature = "admin-api")]
353    pub const fn with_admin_api_ip(mut self, ip: SocketAddr) -> Self {
354        self.admin_api_address = ip;
355        self
356    }
357}
358
359#[cfg(not(feature = "rocksdb"))]
360type DbBuilderType = Database<in_memory::Storage>;
361#[cfg(feature = "rocksdb")]
362type DbBuilderType = DbOptions;
363
364#[cfg(feature = "rocksdb")]
365mod rocksdb {
366    use super::*;
367    use crate::storage::{
368        options::DbOptions,
369        rocksdb,
370    };
371    use storage::database::spec::DbSpecWithUserOptions;
372    use storage_rocksdb::RocksDb;
373
374    // We still implement this setter, so people can manually configure RocksDB
375    impl<S> DomainBuilder<S> {
376        /// Use RocksDb with the given configuration as storage
377        pub fn with_rocksdb(self, db_options: DbOptions) -> DomainBuilder<DbOptions> {
378            DomainBuilder {
379                config: self.config,
380                keypair: self.keypair,
381                base_layer: self.base_layer,
382                proving: self.proving,
383                storage: db_options,
384                admin_api_address: self.admin_api_address,
385            }
386        }
387    }
388
389    impl DomainBuilder<DbOptions> {
390        /// Build the domain to get its handles.
391        pub async fn build(self) -> Result<Domain<rocksdb::Storage>, Error> {
392            let db_spec_with_options = DbSpecWithUserOptions::new(self.storage.clone().into());
393            let storage = RocksDb::new_static(db_spec_with_options).context(StorageSnafu)?;
394            self.with_storage(storage).build().await
395        }
396    }
397}
398
399impl<S> DomainBuilder<Database<S>>
400where
401    S: KeyValueStorage<ColumnFamilyIdentifier = ColumnFamilies, Error = StorageError>
402        + Send
403        + Sync
404        + 'static,
405{
406    /// Build the domain to get its handles.
407    pub async fn build(self) -> Result<Domain<S>, Error> {
408        let Self {
409            config,
410            keypair,
411            proving,
412            storage: db,
413            base_layer,
414            admin_api_address,
415        } = self;
416
417        let (base_layer, seed_vaults) = match base_layer {
418            BaseLayerOptions::Rpc { url } => {
419                let rpc_client = rpc::BaseLayer::new(url?.to_string(), keypair)
420                    .await
421                    .boxed()
422                    .context(BaseLayerSnafu)?;
423
424                rpc_client
425                    .check_valid_domain_agreement(config.shard)
426                    .await
427                    .or_else(|err| match err {
428                        // actually return/fail for RPC errors
429                        domain_agreement::Error::Rpc { source } => {
430                            Err(source).context(BaseLayerSnafu)
431                        },
432                        domain_agreement::Error::NotFound
433                        | domain_agreement::Error::WrongOperator { .. } => Ok(()),
434                    })?;
435
436                // Fetches ALL non-empty vaults of the shard from the RPC and
437                // will thus overwrite any vaults stored in an existing RocksDB.
438                // Therefore, we only want to overwrite if there is no vault
439                // data already in the storage (i.e. fresh RocksDB or in-memory).
440                // TODO(#2021): remove this
441                let seed_vaults = if has_vaults(db.storage()) {
442                    tracing::info!(
443                        "Seed vaults already present in storage. Not fetching from RPC."
444                    );
445                    HashMap::new()
446                } else {
447                    rpc_client
448                        .get_all_vaults_of(config.shard)
449                        .await
450                        .boxed()
451                        .context(BaseLayerSnafu)?
452                };
453
454                (BaseLayer::Rpc(rpc_client), seed_vaults)
455            },
456
457            BaseLayerOptions::Mock {
458                vaults: mock_vaults,
459            } => {
460                // Clone local mock vaults to DB seed
461                let seed_vaults = mock_vaults
462                    .iter()
463                    .filter(|(id, _)| id.shard() == config.shard.get())
464                    .map(|(address, vault)| (address.owner(), vault.clone()))
465                    .collect();
466
467                tracing::info!("Starting domain with a mock base layer RPC.");
468                (
469                    BaseLayer::Mock(mock::BaseLayer::new(mock_vaults)),
470                    seed_vaults,
471                )
472            },
473        };
474
475        tracing::info!(
476            "The domain will be started with {} seed vaults.",
477            seed_vaults.len()
478        );
479
480        Ok(Domain::new(
481            config,
482            proving,
483            db,
484            base_layer,
485            seed_vaults,
486            admin_api_address,
487        ))
488    }
489}
490
491fn has_vaults<S>(storage: &S) -> bool
492where
493    S: KeyValueStorage<ColumnFamilyIdentifier = ColumnFamilies>,
494{
495    matches!(
496        storage.iter(ColumnFamilies::FinalizedVaults.name()).next(),
497        Some(Ok(_))
498    )
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use base_sdk::crypto::ed25519::test_helpers::KeyDispenser as Ed25519KeyDispenser;
505    use crypto::test_helper::KeyDispenser as _;
506
507    /// Test to ensure that build is available for in-memory storage
508    #[cfg(not(feature = "rocksdb"))]
509    #[tokio::test]
510    async fn test_from_config_in_mem() {
511        let config = Config::with_defaults(
512            NonZero::new(1).unwrap(),
513            Ed25519KeyDispenser::default().dispense(),
514        );
515
516        assert!(Domain::from_config(config).build().await.is_ok())
517    }
518
519    /// Test to ensure that `with_rocksdb` is available on a rocksdb-enabled
520    /// builder. The assertion is more for style.
521    #[cfg(feature = "rocksdb")]
522    #[test]
523    fn test_with_rocksdb() {
524        use std::path::PathBuf;
525
526        let db_options = DbOptions::default().with_db_prefix_path(PathBuf::from("/test"));
527        let builder = Domain::builder(
528            NonZero::new(1).unwrap(),
529            Ed25519KeyDispenser::default().dispense(),
530        )
531        .with_rocksdb(db_options.clone());
532
533        assert_eq!(builder.storage, db_options);
534    }
535}