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}/{encoded_signature}"
122            ))
123            .send()
124            .await
125            .map_err(Into::into)
126    }
127}
128
129/// Version 0.1
130pub mod v0_1 {
131    use hotshot_builder_api::v0_1::block_info::{
132        AvailableBlockData, AvailableBlockHeaderInputV2, AvailableBlockHeaderInputV2Either,
133        AvailableBlockHeaderInputV2Legacy,
134    };
135    pub use hotshot_builder_api::v0_1::Version;
136    use hotshot_types::{
137        constants::LEGACY_BUILDER_MODULE,
138        traits::{node_implementation::NodeType, signature_key::SignatureKey},
139        utils::BuilderCommitment,
140    };
141    use tagged_base64::TaggedBase64;
142    use vbs::BinarySerializer;
143
144    use super::BuilderClientError;
145
146    /// Client for builder API
147    pub type BuilderClient<TYPES> = super::BuilderClient<TYPES, Version>;
148
149    impl<TYPES: NodeType> BuilderClient<TYPES> {
150        /// Claim block header input
151        ///
152        /// # Errors
153        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
154        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
155        pub async fn claim_block_header_input(
156            &self,
157            block_hash: BuilderCommitment,
158            view_number: u64,
159            sender: TYPES::SignatureKey,
160            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
161        ) -> Result<AvailableBlockHeaderInputV2<TYPES>, BuilderClientError> {
162            let encoded_signature: TaggedBase64 = signature.clone().into();
163            self.client
164                .get(&format!(
165                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
166                ))
167                .send()
168                .await
169                .map_err(Into::into)
170        }
171
172        /// Claim block header input, using the legacy `AvailableBlockHeaderInputV2Legacy` type
173        ///
174        /// # Errors
175        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
176        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
177        pub async fn claim_legacy_block_header_input(
178            &self,
179            block_hash: BuilderCommitment,
180            view_number: u64,
181            sender: TYPES::SignatureKey,
182            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
183        ) -> Result<AvailableBlockHeaderInputV2Legacy<TYPES>, BuilderClientError> {
184            let encoded_signature: TaggedBase64 = signature.clone().into();
185            self.client
186                .get(&format!(
187                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
188                ))
189                .send()
190                .await
191                .map_err(Into::into)
192        }
193
194        /// Claim block header input, preferring the current `AvailableBlockHeaderInputV2` type but falling back to
195        /// the `AvailableBlockHeaderInputV2Legacy` type
196        ///
197        /// # Errors
198        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
199        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
200        pub async fn claim_either_block_header_input(
201            &self,
202            block_hash: BuilderCommitment,
203            view_number: u64,
204            sender: TYPES::SignatureKey,
205            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
206        ) -> Result<AvailableBlockHeaderInputV2Either<TYPES>, BuilderClientError> {
207            let encoded_signature: TaggedBase64 = signature.clone().into();
208            let result = self.client
209                .get::<Vec<u8>>(&format!(
210                    "{LEGACY_BUILDER_MODULE}/claimheaderinput/v2/{block_hash}/{view_number}/{sender}/{encoded_signature}"
211                ))
212                .bytes()
213                .await
214                .map_err(Into::<BuilderClientError>::into)?;
215
216            // Manually deserialize the result as one of the enum types. Bincode doesn't support deserialize_any,
217            // so we can't go directly into our target type.
218
219            if let Ok(available_block_header_input_v2) = vbs::Serializer::<Version>::deserialize::<
220                AvailableBlockHeaderInputV2<TYPES>,
221            >(&result)
222            {
223                Ok(AvailableBlockHeaderInputV2Either::Current(
224                    available_block_header_input_v2,
225                ))
226            } else {
227                vbs::Serializer::<Version>::deserialize::<AvailableBlockHeaderInputV2Legacy<TYPES>>(
228                    &result,
229                )
230                .map_err(|e| BuilderClientError::Api(format!("Failed to deserialize: {e:?}")))
231                .map(AvailableBlockHeaderInputV2Either::Legacy)
232            }
233        }
234
235        /// Claim block
236        ///
237        /// # Errors
238        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
239        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
240        pub async fn claim_block(
241            &self,
242            block_hash: BuilderCommitment,
243            view_number: u64,
244            sender: TYPES::SignatureKey,
245            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
246        ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
247            let encoded_signature: TaggedBase64 = signature.clone().into();
248            self.client
249                .get(&format!(
250                    "{LEGACY_BUILDER_MODULE}/claimblock/{block_hash}/{view_number}/{sender}/{encoded_signature}"
251                ))
252                .send()
253                .await
254                .map_err(Into::into)
255        }
256
257        /// Claim block and provide the number of nodes information to the builder for VID
258        /// computation.
259        ///
260        /// # Errors
261        /// - [`BuilderClientError::BlockNotFound`] if block isn't available
262        /// - [`BuilderClientError::Api`] if API isn't responding or responds incorrectly
263        pub async fn claim_block_with_num_nodes(
264            &self,
265            block_hash: BuilderCommitment,
266            view_number: u64,
267            sender: TYPES::SignatureKey,
268            signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
269            num_nodes: usize,
270        ) -> Result<AvailableBlockData<TYPES>, BuilderClientError> {
271            let encoded_signature: TaggedBase64 = signature.clone().into();
272            self.client
273                .get(&format!(
274                    "{LEGACY_BUILDER_MODULE}/claimblockwithnumnodes/{block_hash}/{view_number}/{sender}/{encoded_signature}/{num_nodes}"
275                ))
276                .send()
277                .await
278                .map_err(Into::into)
279        }
280    }
281}
282
283/// Version 0.2. No changes in API
284pub mod v0_2 {
285    use vbs::version::StaticVersion;
286
287    pub use super::v0_1::*;
288
289    /// Builder API version
290    pub type Version = StaticVersion<0, 2>;
291}