delta_domain_sdk/
client.rs

1//! # Client
2//!
3//! [`DomainClient`] to perform actions on a running domain.
4
5use crate::{
6    base_layer::{
7        BaseLayer,
8        BaseLayerPortError,
9    },
10    domain_agreement,
11};
12use base_sdk::{
13    core::{
14        Planck,
15        SdlId,
16        Shard,
17    },
18    sdl::VerificationKey,
19};
20use domain_runtime::endpoint::Update;
21use snafu::{
22    ResultExt,
23    Snafu,
24};
25use std::num::NonZero;
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
43        source: BaseLayerPortError,
44    },
45
46    /// Failed to check for the domain agreement
47    #[snafu(display("Domain agreement check failed: {source}"))]
48    DomainAgreement {
49        /// The underlying error
50        source: domain_agreement::Error,
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        tracing::debug!("Applying {} verifiable(s)", messages.len());
102        self.endpoint
103            .handle_apply(messages)
104            .await
105            .context(EndpointSnafu)
106    }
107
108    /// Submit all diffs that were applied since the last call to the base
109    /// layer and return the hash of the resulting SDL.
110    ///
111    /// # Returns
112    ///
113    /// * `Ok(Some(sdl_id))` - If diffs were successfully submitted, returns the SDL id
114    /// * `Ok(None)` - If there were no pending diffs to submit
115    /// * `Err(error)` - If the submission failed
116    pub async fn submit(&self) -> Result<Option<SdlId>, Error> {
117        let sdl_id = self.endpoint.handle_submit().await.context(EndpointSnafu)?;
118        Ok(sdl_id)
119    }
120
121    /// Prove the SDL specified by the hash.
122    ///
123    /// Generates a proof for the SDL with the given hash and only returns once
124    /// the proof is finished (or fails).
125    ///
126    /// Use [Self::prove_with_local_laws_input] if you need to provide
127    /// additional inputs for the local laws.
128    ///
129    /// # Parameters
130    ///
131    /// * `sdl_id` - The id of the SDL to generate a proof for
132    ///
133    /// # Returns
134    /// Ok(()) if proving succeeded, Error otherwise
135    pub async fn prove(&self, sdl_id: SdlId) -> Result<(), Error> {
136        self.endpoint
137            .handle_prove(sdl_id, None)
138            .await
139            .context(EndpointSnafu)
140    }
141
142    /// Prove the SDL specified by the hash with additional inputs.
143    ///
144    /// Generates a proof for the SDL with the given hash and only returns once
145    /// the proof is finished (or fails).
146    ///
147    /// # Parameters
148    ///
149    /// * `sdl_id` - The id of the SDL to generate a proof for
150    /// * `local_laws_input` - Additional inputs to the configured local laws
151    ///
152    /// # Returns
153    /// Ok(()) if proving succeeded, Error otherwise
154    pub async fn prove_with_local_laws_input(
155        &self,
156        sdl_id: SdlId,
157        local_laws_input: Box<dyn erased_serde::Serialize + Send + Sync>,
158    ) -> Result<(), Error> {
159        self.endpoint
160            .handle_prove(sdl_id, Some(local_laws_input))
161            .await
162            .context(EndpointSnafu)
163    }
164
165    /// Submit the proof of the SDL specified by the hash to the base layer.
166    ///
167    /// # Parameters
168    ///
169    /// * `sdl_id` - The id of the SDL whose proof should be submitted
170    ///
171    /// # Returns
172    ///
173    /// Ok(()) if the proof was successfully submitted, or an Error if it failed
174    pub async fn submit_proof(&self, sdl_id: SdlId) -> Result<(), Error> {
175        self.endpoint
176            .handle_submit_proof(sdl_id)
177            .await
178            .context(EndpointSnafu)
179    }
180
181    /// Subscribes to a stream of events from the running domain.
182    pub fn updates(&self) -> broadcast::Receiver<Update> {
183        self.endpoint.stream_updates()
184    }
185
186    /// Shorthand to apply verifiables, submit an SDL, generate and submit a
187    /// proof
188    ///
189    /// This method combines the following:
190    /// 1. [apply](Self::apply) to apply verifiables to the vaults
191    /// 2. [submit](Self::submit) to batch the changes in an SDL and submit it
192    ///    to the base layer
193    /// 3. [proving](Self::prove) to start proving the SDL
194    /// 4. [submit_proof](Self::submit_proof) to submit the proof to the base
195    ///    layer
196    ///
197    /// This method does not allow setting additional inputs for the local laws.
198    ///
199    /// # Parameters
200    ///
201    /// * `messages` - The verifiable messages to be applied
202    ///
203    /// # Returns
204    ///
205    /// * `Ok(Some(sdl_id))` - Success, an SDL with this id was created & proven
206    /// * `Ok(None)` - Success but there were no state changes to submit
207    /// * `Err(error)` - Any step in the process failed.
208    pub async fn apply_prove_submit(
209        &self,
210        messages: Vec<VerifiableWithDiffs>,
211    ) -> Result<Option<SdlId>, Error> {
212        tracing::debug!(
213            "Starting apply_prove_submit with {} verifiable(s)",
214            messages.len()
215        );
216        self.apply(messages).await?;
217
218        let Some(new_sdl_id) = self.submit().await? else {
219            tracing::debug!("apply_prove_submit: no diffs to submit");
220            return Ok(None);
221        };
222
223        self.prove(new_sdl_id).await?;
224
225        self.submit_proof(new_sdl_id).await?;
226        tracing::debug!("apply_prove_submit completed for SDL {new_sdl_id}");
227        Ok(Some(new_sdl_id))
228    }
229
230    /// Submit a Domain Agreement for this domain.
231    ///
232    /// Having an active domain agreement is a prerequisite to submit transactions to the
233    /// base layer. You can check whether a domain agreement is already active with
234    /// [Self::check_valid_domain_agreement].
235    ///
236    /// Note that after submitting the domain agreement, it still has to be applied on the
237    /// base layer, before the domain is allowed to send transactions.
238    ///
239    /// # Parameters
240    /// - `new_shard_fee` -  amount of native token that will be charged to the
241    ///   domain operator keys's base layer vault for creation of the new shard
242    ///
243    /// # Returns
244    /// - `Ok(())` - if the domain agreement was successfully submitted
245    /// - `Err(_)` - see [crate::domain_agreement::Error] for error interpretation
246    pub async fn submit_domain_agreement(&self, new_shard_fee: Planck) -> Result<(), Error> {
247        self.base_layer
248            .submit_domain_agreement(self.shard, new_shard_fee, self.local_laws_vkey)
249            .await
250            .context(BaseLayerSnafu)
251    }
252
253    /// Check that there is an active Domain Agreement for this domain.
254    ///
255    /// This method can typically be used as a safe-guard before calling
256    /// [Self::submit] or other methods that require an active domain agreement
257    /// for transactions to be accepted.
258    ///
259    /// # Returns
260    /// - `Ok(())` - if there is an active domain agreement for this domain
261    /// - `Err(_)` - see [crate::domain_agreement::Error] for interpretation
262    pub async fn check_valid_domain_agreement(&self) -> Result<(), domain_agreement::Error> {
263        self.base_layer
264            .check_valid_domain_agreement(self.shard)
265            .await
266    }
267
268    /// Ensure that the domain has a valid Domain Agreement.
269    ///
270    /// If no valid Domain Agreement is
271    /// [found](Self::check_valid_domain_agreement), a new one will be
272    /// [submitted](Self::submit_domain_agreement) and awaited up to `timeout`.
273    ///
274    /// # Parameters
275    /// - `new_shard_fee` -  amount of native token that will be charged to the
276    ///   domain operator keys's base layer vault for creation of the new shard
277    /// - `interval` - duration between polling for the the domain agreement
278    /// - `timeout` - maximal duration to await the new domain agreement
279    pub async fn ensure_domain_agreement(&self, new_shard_fee: Planck) -> Result<(), Error> {
280        if self.check_valid_domain_agreement().await.is_ok() {
281            tracing::info!("The domain already has a valid domain agreement.");
282            return Ok(());
283        }
284
285        tracing::info!("Submitting a new domain agreement...");
286        self.submit_domain_agreement(new_shard_fee).await?;
287        let mut updates = self.updates();
288
289        loop {
290            // Since `self.endpoint` holds the `broadcast::Sender` the recv can never error out
291            // so we can safely ignore the else branch here.
292            if let Ok(Update::NewEpoch(_)) = updates.recv().await {
293                tracing::info!("New epoch began, checking for domain agreement");
294                return self
295                    .check_valid_domain_agreement()
296                    .await
297                    .context(DomainAgreementSnafu);
298            }
299        }
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}