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