hotshot_task_impls/
builder.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7use std::time::{Duration, Instant};
8
9use hotshot_builder_api::v0_1::{
10    block_info::AvailableBlockInfo,
11    builder::{BuildError, Error as BuilderApiError},
12};
13use hotshot_types::{
14    constants::LEGACY_BUILDER_MODULE,
15    data::VidCommitment,
16    traits::{node_implementation::NodeType, signature_key::SignatureKey},
17};
18use serde::{Deserialize, Serialize};
19use surf_disco::{client::HealthStatus, Client, Url};
20use tagged_base64::TaggedBase64;
21use thiserror::Error;
22use tokio::time::sleep;
23use vbs::version::StaticVersionType;
24
25#[derive(Debug, Error, Serialize, Deserialize)]
26/// Represents errors that can occur while interacting with the builder
27pub enum BuilderClientError {
28    /// The requested block was not found
29    #[error("Requested block not found")]
30    BlockNotFound,
31
32    /// The requested block was missing
33    #[error("Requested block was missing")]
34    BlockMissing,
35
36    /// Generic error while accessing the API
37    #[error("Builder API error: {0}")]
38    Api(String),
39}
40
41impl From<BuilderApiError> for BuilderClientError {
42    fn from(value: BuilderApiError) -> Self {
43        match value {
44            BuilderApiError::Request(source) | BuilderApiError::TxnUnpack(source) => {
45                Self::Api(source.to_string())
46            },
47            BuilderApiError::TxnSubmit(source) | BuilderApiError::BuilderAddress(source) => {
48                Self::Api(source.to_string())
49            },
50            BuilderApiError::Custom { message, .. } => Self::Api(message),
51            BuilderApiError::BlockAvailable { source, .. }
52            | BuilderApiError::BlockClaim { source, .. } => match source {
53                BuildError::NotFound => Self::BlockNotFound,
54                BuildError::Missing => Self::BlockMissing,
55                BuildError::Error(message) => Self::Api(message),
56            },
57            BuilderApiError::TxnStat(source) => Self::Api(source.to_string()),
58        }
59    }
60}
61
62/// Client for builder API
63pub struct BuilderClient<TYPES: NodeType, Ver: StaticVersionType> {
64    /// Underlying surf_disco::Client for the legacy builder api
65    client: Client<BuilderApiError, Ver>,
66    /// Marker for [`NodeType`] used here
67    _marker: std::marker::PhantomData<TYPES>,
68}
69
70impl<TYPES: NodeType, Ver: StaticVersionType> BuilderClient<TYPES, Ver> {
71    /// Construct a new client from base url
72    ///
73    /// # Panics
74    ///
75    /// If the URL is malformed.
76    pub fn new(base_url: impl Into<Url>) -> Self {
77        let url = base_url.into();
78
79        Self {
80            client: Client::builder(url.clone())
81                .set_timeout(Some(Duration::from_secs(2)))
82                .build(),
83            _marker: std::marker::PhantomData,
84        }
85    }
86
87    /// Wait for server to become available
88    /// Returns `false` if server doesn't respond
89    /// with OK healthcheck before `timeout`
90    pub async fn connect(&self, timeout: Duration) -> bool {
91        let timeout = Instant::now() + timeout;
92        let mut backoff = Duration::from_millis(50);
93        while Instant::now() < timeout {
94            if matches!(
95                self.client.healthcheck::<HealthStatus>().await,
96                Ok(HealthStatus::Available)
97            ) {
98                return true;
99            }
100            sleep(backoff).await;
101            backoff *= 2;
102        }
103        false
104    }
105
106    /// Query builder for available blocks
107    ///
108    /// # Errors
109    /// - [`BuilderClientError::BlockNotFound`] if blocks aren't available for this parent
110    /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
111    pub async fn available_blocks(
112        &self,
113        parent: VidCommitment,
114        view_number: u64,
115        sender: TYPES::SignatureKey,
116        signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
117    ) -> Result<Vec<AvailableBlockInfo<TYPES>>, BuilderClientError> {
118        let encoded_signature: TaggedBase64 = signature.clone().into();
119        self.client
120            .get(&format!(
121                "{LEGACY_BUILDER_MODULE}/availableblocks/{parent}/{view_number}/{sender}/\
122                 {encoded_signature}"
123            ))
124            .send()
125            .await
126            .map_err(Into::into)
127    }
128}
129
130/// Version 0.1
131pub mod v0_1 {
132    use hotshot_builder_api::v0_1::block_info::{
133        AvailableBlockData, AvailableBlockHeaderInputV2, AvailableBlockHeaderInputV2Either,
134        AvailableBlockHeaderInputV2Legacy,
135    };
136    pub use hotshot_builder_api::v0_1::Version;
137    use hotshot_types::{
138        constants::LEGACY_BUILDER_MODULE,
139        traits::{node_implementation::NodeType, signature_key::SignatureKey},
140        utils::BuilderCommitment,
141    };
142    use tagged_base64::TaggedBase64;
143    use vbs::BinarySerializer;
144
145    use super::BuilderClientError;
146
147    /// Client for builder API
148    pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, Version>;
149
150    impl<TYPES: NodeType> BuilderClient<TYPES> {
151        /// Claim block header input
152        ///
153        /// # Errors
154        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
155        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
156        pub async fn claim_block_header_input(
157            &self,
158            block_hash: BuilderCommitment,
159            view_number: u64,
160            sender: TYPES::SignatureKey,
161            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
162        ) -> Result<AvailableBlockHeaderInputV2<TYPES>, BuilderClientError> {
163            let encoded_signature: TaggedBase64 = signature.clone().into();
164            self.client
165                .get(&format!(
166                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
167                     {sender}/{encoded_signature}"
168                ))
169                .send()
170                .await
171                .map_err(Into::into)
172        }
173
174        /// Claim block header input, using the legacy `AvailableBlockHeaderInputV2Legacy` type
175        ///
176        /// # Errors
177        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
178        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
179        pub async fn claim_legacy_block_header_input(
180            &self,
181            block_hash: BuilderCommitment,
182            view_number: u64,
183            sender: TYPES::SignatureKey,
184            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
185        ) -> Result<AvailableBlockHeaderInputV2Legacy<TYPES>, BuilderClientError> {
186            let encoded_signature: TaggedBase64 = signature.clone().into();
187            self.client
188                .get(&format!(
189                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
190                     {sender}/{encoded_signature}"
191                ))
192                .send()
193                .await
194                .map_err(Into::into)
195        }
196
197        /// Claim block header input, preferring the current `AvailableBlockHeaderInputV2` type but falling back to
198        /// the `AvailableBlockHeaderInputV2Legacy` type
199        ///
200        /// # Errors
201        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
202        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
203        pub async fn claim_either_block_header_input(
204            &self,
205            block_hash: BuilderCommitment,
206            view_number: u64,
207            sender: TYPES::SignatureKey,
208            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
209        ) -> Result<AvailableBlockHeaderInputV2Either<TYPES>, BuilderClientError> {
210            let encoded_signature: TaggedBase64 = signature.clone().into();
211            let result = self
212                .client
213                .get::<Vec<u8>>(&format!(
214                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/\
215                     {sender}/{encoded_signature}"
216                ))
217                .bytes()
218                .await
219                .map_err(Into::<BuilderClientError>::into)?;
220
221            // Manually deserialize the result as one of the enum types. Bincode doesn't support deserialize_any,
222            // so we can't go directly into our target type.
223
224            if let Ok(available_block_header_input_v2) = vbs::Serializer::<Version>::deserialize::<
225                AvailableBlockHeaderInputV2<TYPES>,
226            >(&result)
227            {
228                Ok(AvailableBlockHeaderInputV2Either::Current(
229                    available_block_header_input_v2,
230                ))
231            } else {
232                vbs::Serializer::<Version>::deserialize::<AvailableBlockHeaderInputV2Legacy<TYPES>>(
233                    &result,
234                )
235                .map_err(|e| BuilderClientError::Api(format!("Failed to deserialize: {e:?}")))
236                .map(AvailableBlockHeaderInputV2Either::Legacy)
237            }
238        }
239
240        /// Claim block
241        ///
242        /// # Errors
243        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
244        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
245        pub async fn claim_block(
246            &self,
247            block_hash: BuilderCommitment,
248            view_number: u64,
249            sender: TYPES::SignatureKey,
250            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
251        ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
252            let encoded_signature: TaggedBase64 = signature.clone().into();
253            self.client
254                .get(&format!(
255                    "{LEGACY_BUILDER_MODULE}/claimblock/{block_hash}/{view_number}/{sender}/\
256                     {encoded_signature}"
257                ))
258                .send()
259                .await
260                .map_err(Into::into)
261        }
262
263        /// Claim block and provide the number of nodes information to the builder for VID
264        /// computation.
265        ///
266        /// # Errors
267        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
268        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
269        pub async fn claim_block_with_num_nodes(
270            &self,
271            block_hash: BuilderCommitment,
272            view_number: u64,
273            sender: TYPES::SignatureKey,
274            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
275            num_nodes: usize,
276        ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
277            let encoded_signature: TaggedBase64 = signature.clone().into();
278            self.client
279                .get(&format!(
280                    "{LEGACY_BUILDER_MODULE}/claimblockwithnumnodes/{block_hash}/{view_number}/\
281                     {sender}/{encoded_signature}/{num_nodes}"
282                ))
283                .send()
284                .await
285                .map_err(Into::into)
286        }
287    }
288}
289
290/// Version 0.2. No changes in API
291pub mod v0_2 {
292    use vbs::version::StaticVersion;
293
294    pub use super::v0_1::*;
295
296    /// Builder API version
297    pub type Version = StaticVersion<0, 2>;
298}