delta_domain_sdk/
client.rs

1//! # Client
2//!
3//! [`DomainClient`] to perform actions on a running domain.
4
5use crate::{
6    base_layer::BaseLayer,
7    domain_agreement,
8};
9use base_sdk::{
10    core::{
11        Planck,
12        Shard,
13    },
14    crypto::HashDigest,
15    sdl::VerificationKey,
16};
17use domain_runtime::sdls::types::SdlUpdate;
18use snafu::{
19    ResultExt,
20    Snafu,
21};
22use std::{
23    num::NonZero,
24    time::Duration,
25};
26use tokio::sync::broadcast;
27use verifiable::types::VerifiableWithDiffs;
28
29/// Errors occurring in the [DomainClient]
30#[derive(Debug, Snafu)]
31pub enum Error {
32    /// Error from the runtime task endpoint
33    #[snafu(display("{source}"))]
34    Endpoint {
35        /// The underlying endpoint error
36        source: domain_runtime::endpoint::Error,
37    },
38
39    /// Base layer client initialization or communication failed
40    #[snafu(display("Base layer error: {source}"))]
41    BaseLayer {
42        /// The underlying error from the base layer client
43        source: Box<dyn std::error::Error + Send + Sync>,
44    },
45
46    /// Timeout error
47    #[snafu(display("Timeout elapsed: {source}"))]
48    Timeout {
49        /// The underlying timeout error
50        source: tokio::time::error::Elapsed,
51    },
52}
53
54/// Client to perform actions on a running domain.
55///
56/// The client is cloneable and can be shared across tasks. It provides methods
57/// to apply verifiable messages, submit state diffs to the base layer, and
58/// generate and submit proofs.
59///
60/// Without [running](crate::runner::Runner::run) the domain first, actions
61/// performed by the client cannot be executed.
62#[derive(Debug, Clone)]
63pub struct DomainClient {
64    // data
65    shard: NonZero<Shard>,
66    local_laws_vkey: Option<VerificationKey>,
67
68    // clients, endpoints
69    endpoint: domain_runtime::Endpoint,
70    base_layer: BaseLayer,
71}
72
73impl DomainClient {
74    pub(crate) const fn new(
75        shard: NonZero<Shard>,
76        local_laws_vkey: Option<VerificationKey>,
77        endpoint: domain_runtime::Endpoint,
78        base_layer: BaseLayer,
79    ) -> Self {
80        Self {
81            shard,
82            local_laws_vkey,
83            endpoint,
84            base_layer,
85        }
86    }
87
88    /// Apply state diffs to the vaults
89    ///
90    /// Changes to the vaults are visible immediately locally. The resulting state
91    /// diff is buffered until a call to [Self::submit].
92    ///
93    /// # Parameters
94    ///
95    /// * `messages` - The verifiable messages with diffs to be applied
96    ///
97    /// # Returns
98    ///
99    /// Ok(()) if application succeeded, or an Error if it failed
100    pub async fn apply(&self, messages: Vec<VerifiableWithDiffs>) -> Result<(), Error> {
101        self.endpoint
102            .handle_apply(messages)
103            .await
104            .context(EndpointSnafu)
105    }
106
107    /// Submit all diffs that were applied since the last call to the base
108    /// layer and return the hash of the resulting SDL.
109    ///
110    /// # Returns
111    ///
112    /// * `Ok(Some(hash))` - If diffs were successfully submitted, returns the SDL hash
113    /// * `Ok(None)` - If there were no pending diffs to submit
114    /// * `Err(error)` - If the submission failed
115    pub async fn submit(&self) -> Result<Option<HashDigest>, Error> {
116        let hash = self.endpoint.handle_submit().await.context(EndpointSnafu)?;
117        Ok(hash)
118    }
119
120    /// Prove the SDL specified by the hash.
121    ///
122    /// Generates a proof for the SDL with the given hash and only returns once
123    /// the proof is finished (or fails).
124    ///
125    /// Use [Self::prove_with_local_laws_input] if you need to provide
126    /// additional inputs for the local laws.
127    ///
128    /// # Parameters
129    ///
130    /// * `sdl` - The hash of the SDL to generate a proof for
131    ///
132    /// # Returns
133    /// Ok(()) if proving succeeded, Error otherwise
134    pub async fn prove(&self, sdl: HashDigest) -> Result<(), Error> {
135        self.endpoint
136            .handle_prove(sdl, None)
137            .await
138            .context(EndpointSnafu)
139    }
140
141    /// Prove the SDL specified by the hash with additional inputs.
142    ///
143    /// Generates a proof for the SDL with the given hash and only returns once
144    /// the proof is finished (or fails).
145    ///
146    /// # Parameters
147    ///
148    /// * `sdl` - The hash of the SDL to generate a proof for
149    /// * `local_laws_input` - Additional inputs to the configured local laws
150    ///
151    /// # Returns
152    /// Ok(()) if proving succeeded, Error otherwise
153    pub async fn prove_with_local_laws_input(
154        &self,
155        sdl: HashDigest,
156        local_laws_input: Vec<u8>,
157    ) -> Result<(), Error> {
158        self.endpoint
159            .handle_prove(sdl, Some(local_laws_input))
160            .await
161            .context(EndpointSnafu)
162    }
163
164    /// Submit the proof of the SDL specified by the hash to the base layer.
165    ///
166    /// # Parameters
167    ///
168    /// * `sdl` - The hash of the SDL whose proof should be submitted
169    ///
170    /// # Returns
171    ///
172    /// Ok(()) if the proof was successfully submitted, or an Error if it failed
173    pub async fn submit_proof(&self, sdl: HashDigest) -> Result<(), Error> {
174        self.endpoint
175            .handle_submit_proof(sdl)
176            .await
177            .context(EndpointSnafu)
178    }
179
180    /// Subscribes to a stream of events from the running domain.
181    pub fn updates(&self) -> broadcast::Receiver<SdlUpdate> {
182        self.endpoint.stream_updates()
183    }
184
185    /// Shorthand to apply verifiables, submit an SDL, generate and submit a
186    /// proof
187    ///
188    /// This method combines the following:
189    /// 1. [apply](Self::apply) to apply verifiables to the vaults
190    /// 2. [submit](Self::submit) to batch the changes in an SDL and submit it
191    ///    to the base layer
192    /// 3. [proving](Self::prove) to start proving the SDL
193    /// 4. [submit_proof](Self::submit_proof) to submit the proof to the base
194    ///    layer
195    ///
196    /// This method does not allow setting additional inputs for the local laws.
197    ///
198    /// # Parameters
199    ///
200    /// * `messages` - The verifiable messages to be applied
201    ///
202    /// # Returns
203    ///
204    /// * `Ok(Some(hash))` - Success, an SDL with this hash was created & proven
205    /// * `Ok(None)` - Success but there were no state changes to submit
206    /// * `Err(error)` - Any step in the process failed.
207    pub async fn apply_prove_submit(
208        &self,
209        messages: Vec<VerifiableWithDiffs>,
210    ) -> Result<Option<HashDigest>, Error> {
211        self.apply(messages).await?;
212
213        let Some(new_hash) = self.submit().await? else {
214            return Ok(None);
215        };
216
217        self.prove(new_hash).await?;
218
219        self.submit_proof(new_hash).await?;
220        Ok(Some(new_hash))
221    }
222
223    /// Submit a Domain Agreement for this domain.
224    ///
225    /// Having an active domain agreement is a prerequisite to submit transactions to the
226    /// base layer. You can check whether a domain agreement is already active with
227    /// [Self::check_valid_domain_agreement].
228    ///
229    /// Note that after submitting the domain agreement, it still has to be applied on the
230    /// base layer, before the domain is allowed to send transactions.
231    ///
232    /// # Parameters
233    /// - `new_shard_fee` -  amount of native token that will be charged to the
234    ///   domain operator keys's base layer vault for creation of the new shard
235    ///
236    /// # Returns
237    /// - `Ok(())` - if the domain agreement was successfully submitted
238    /// - `Err(_)` - see [crate::domain_agreement::Error] for error interpretation
239    pub async fn submit_domain_agreement(&self, new_shard_fee: Planck) -> Result<(), Error> {
240        self.base_layer
241            .submit_domain_agreement(self.shard, new_shard_fee, self.local_laws_vkey)
242            .await
243            .boxed()
244            .context(BaseLayerSnafu)
245    }
246
247    /// Check that there is an active Domain Agreement for this domain.
248    ///
249    /// This method can typically be used as a safe-guard before calling
250    /// [Self::submit] or other methods that require an active domain agreement
251    /// for transactions to be accepted.
252    ///
253    /// # Returns
254    /// - `Ok(())` - if there is an active domain agreement for this domain
255    /// - `Err(_)` - see [crate::domain_agreement::Error] for interpretation
256    pub async fn check_valid_domain_agreement(&self) -> Result<(), domain_agreement::Error> {
257        self.base_layer
258            .check_valid_domain_agreement(self.shard)
259            .await
260    }
261
262    /// Ensure that the domain has a valid Domain Agreement.
263    ///
264    /// If no valid Domain Agreement is
265    /// [found](Self::check_valid_domain_agreement), a new one will be
266    /// [submitted](Self::submit_domain_agreement) and awaited up to `timeout`.
267    ///
268    /// # Parameters
269    /// - `new_shard_fee` -  amount of native token that will be charged to the
270    ///   domain operator keys's base layer vault for creation of the new shard
271    /// - `interval` - duration between polling for the the domain agreement
272    /// - `timeout` - maximal duration to await the new domain agreement
273    pub async fn ensure_domain_agreement(
274        &self,
275        new_shard_fee: Planck,
276        interval: Duration,
277        timeout: Duration,
278    ) -> Result<(), Error> {
279        if self.check_valid_domain_agreement().await.is_ok() {
280            tracing::info!("The domain already has a valid domain agreement.");
281            return Ok(());
282        }
283
284        tracing::info!("Submitting a new domain agreement...");
285        self.submit_domain_agreement(new_shard_fee).await?;
286
287        tokio::time::timeout(timeout, async {
288            loop {
289                tokio::time::sleep(interval).await;
290                if self.check_valid_domain_agreement().await.is_ok() {
291                    tracing::info!("The domain agreement is valid.");
292                    break;
293                } else {
294                    tracing::debug!("The domain agreement is not yet valid. Waiting...");
295                }
296            }
297        })
298        .await
299        .context(TimeoutSnafu)
300    }
301}
302
303#[cfg(test)]
304mod test {
305    use super::*;
306    use crate::base_layer::mock;
307
308    impl DomainClient {
309        /// Get access to the underlying mock base layer, if configured.
310        ///
311        /// This is a test-only escape hatch for asserting on base-layer interactions
312        /// (e.g. submitted transaction nonces) without going through the public API.
313        pub fn mock_rpc(&self) -> mock::BaseLayer {
314            let BaseLayer::Mock(inner) = &self.base_layer else {
315                panic!("Called `mock_rpc` on a domain with a real RPC")
316            };
317            inner.clone()
318        }
319    }
320}