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}