Skip to main content

hotshot_builder_api/v0_1/
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::path::PathBuf;
8
9use clap::Args;
10use committable::Committable;
11use futures::FutureExt;
12use hotshot_types::{traits::node_implementation::NodeType, utils::BuilderCommitment};
13use serde::{Deserialize, Serialize};
14use tagged_base64::TaggedBase64;
15use thiserror::Error;
16// `RequestError` is re-exported because it is embedded in this module's wire error type
17// (`Error::Request`/`Error::TxnUnpack`); servers reimplementing this API against a different
18// HTTP framework need to construct it without a direct tide-disco dependency.
19pub use tide_disco::RequestError;
20use tide_disco::{Api, RequestParams, StatusCode, api::ApiError, method::ReadState};
21use vbs::version::StaticVersionType;
22
23use super::{
24    Version,
25    block_info::AvailableBlockHeaderInputV2,
26    data_source::{AcceptsTxnSubmits, BuilderDataSource},
27};
28use crate::api::load_api;
29
30#[derive(Args, Default)]
31pub struct Options {
32    #[arg(long = "builder-api-path", env = "HOTSHOT_BUILDER_API_PATH")]
33    pub api_path: Option<PathBuf>,
34
35    /// Additional API specification files to merge with `builder-api-path`.
36    ///
37    /// These optional files may contain route definitions for application-specific routes that have
38    /// been added as extensions to the basic builder API.
39    #[arg(
40        long = "builder-extension",
41        env = "HOTSHOT_BUILDER_EXTENSIONS",
42        value_delimiter = ','
43    )]
44    pub extensions: Vec<toml::Value>,
45}
46
47#[derive(Clone, Debug, Error, Deserialize, Serialize)]
48pub enum BuildError {
49    #[error("The requested resource does not exist or is not known to this builder service")]
50    NotFound,
51    #[error("The requested resource exists but is not currently available")]
52    Missing,
53    #[error("Error trying to fetch the requested resource: {0}")]
54    Error(String),
55}
56
57/// Enum to keep track on status of a transaction
58#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
59pub enum TransactionStatus {
60    Pending,
61    Sequenced { leaf: u64 },
62    Rejected { reason: String }, // Rejection reason is in the String format
63    Unknown,
64}
65
66#[derive(Clone, Debug, Error, Deserialize, Serialize)]
67pub enum Error {
68    #[error("Error processing request: {0}")]
69    Request(#[from] RequestError),
70    #[error("Error building block from {resource}: {source}")]
71    BlockAvailable {
72        source: BuildError,
73        resource: String,
74    },
75    #[error("Error claiming block {resource}: {source}")]
76    BlockClaim {
77        source: BuildError,
78        resource: String,
79    },
80    #[error("Error unpacking transactions: {0}")]
81    TxnUnpack(RequestError),
82    #[error("Error submitting transaction: {0}")]
83    TxnSubmit(BuildError),
84    #[error("Error getting builder address: {0}")]
85    BuilderAddress(#[from] BuildError),
86    #[error("Error getting transaction status: {0}")]
87    TxnStat(BuildError),
88    #[error("Custom error {status}: {message}")]
89    Custom { message: String, status: StatusCode },
90}
91
92impl tide_disco::error::Error for Error {
93    fn catch_all(status: StatusCode, msg: String) -> Self {
94        Error::Custom {
95            message: msg,
96            status,
97        }
98    }
99
100    fn status(&self) -> StatusCode {
101        match self {
102            Error::Request { .. } => StatusCode::BAD_REQUEST,
103            Error::BlockAvailable { source, .. } | Error::BlockClaim { source, .. } => match source
104            {
105                BuildError::NotFound => StatusCode::NOT_FOUND,
106                BuildError::Missing => StatusCode::NOT_FOUND,
107                BuildError::Error { .. } => StatusCode::INTERNAL_SERVER_ERROR,
108            },
109            Error::TxnUnpack { .. } => StatusCode::BAD_REQUEST,
110            Error::TxnSubmit { .. } => StatusCode::INTERNAL_SERVER_ERROR,
111            Error::Custom { status, .. } => *status,
112            Error::BuilderAddress { .. } => StatusCode::INTERNAL_SERVER_ERROR,
113            Error::TxnStat { .. } => StatusCode::INTERNAL_SERVER_ERROR,
114        }
115    }
116}
117
118impl http_client::ClientError for Error {
119    fn catch_all(status: http_client::StatusCode, msg: String) -> Self {
120        Error::Custom {
121            message: msg,
122            status: status.into(),
123        }
124    }
125
126    fn status(&self) -> http_client::StatusCode {
127        disco_types::error::Error::status(self).into()
128    }
129}
130
131pub(crate) fn try_extract_param<T: for<'a> TryFrom<&'a TaggedBase64>>(
132    params: &RequestParams,
133    param_name: &str,
134) -> Result<T, Error> {
135    params
136        .param(param_name)?
137        .as_tagged_base64()?
138        .try_into()
139        .map_err(|_| Error::Custom {
140            message: format!("Invalid {param_name}"),
141            status: StatusCode::UNPROCESSABLE_ENTITY,
142        })
143}
144
145pub fn define_api<State, Types: NodeType>(
146    options: &Options,
147) -> Result<Api<State, Error, Version>, ApiError>
148where
149    State: 'static + Send + Sync + ReadState,
150    <State as ReadState>::State: Send + Sync + BuilderDataSource<Types>,
151{
152    let mut api = load_api::<State, Error, Version>(
153        options.api_path.as_ref(),
154        include_str!("../../api/v0_1/builder.toml"),
155        options.extensions.clone(),
156    )?;
157    api.with_version("0.1.0".parse().unwrap())
158        .get("available_blocks", |req, state| {
159            async move {
160                let hash = req.blob_param("parent_hash")?;
161                let view_number = req.integer_param("view_number")?;
162                let signature = try_extract_param(&req, "signature")?;
163                let sender = try_extract_param(&req, "sender")?;
164                state
165                    .available_blocks(&hash, view_number, sender, &signature)
166                    .await
167                    .map_err(|source| Error::BlockAvailable {
168                        source,
169                        resource: hash.to_string(),
170                    })
171            }
172            .boxed()
173        })?
174        .get("claim_block", |req, state| {
175            async move {
176                let block_hash: BuilderCommitment = req.blob_param("block_hash")?;
177                let view_number = req.integer_param("view_number")?;
178                let signature = try_extract_param(&req, "signature")?;
179                let sender = try_extract_param(&req, "sender")?;
180                state
181                    .claim_block(&block_hash, view_number, sender, &signature)
182                    .await
183                    .map_err(|source| Error::BlockClaim {
184                        source,
185                        resource: block_hash.to_string(),
186                    })
187            }
188            .boxed()
189        })?
190        .get("claim_block_with_num_nodes", |req, state| {
191            async move {
192                let block_hash: BuilderCommitment = req.blob_param("block_hash")?;
193                let view_number = req.integer_param("view_number")?;
194                let signature = try_extract_param(&req, "signature")?;
195                let sender = try_extract_param(&req, "sender")?;
196                let num_nodes = req.integer_param("num_nodes")?;
197                state
198                    .claim_block_with_num_nodes(
199                        &block_hash,
200                        view_number,
201                        sender,
202                        &signature,
203                        num_nodes,
204                    )
205                    .await
206                    .map_err(|source| Error::BlockClaim {
207                        source,
208                        resource: block_hash.to_string(),
209                    })
210            }
211            .boxed()
212        })?
213        .get("claim_header_input", |req, state| {
214            async move {
215                let block_hash: BuilderCommitment = req.blob_param("block_hash")?;
216                let view_number = req.integer_param("view_number")?;
217                let signature = try_extract_param(&req, "signature")?;
218                let sender = try_extract_param(&req, "sender")?;
219                state
220                    .claim_block_header_input(&block_hash, view_number, sender, &signature)
221                    .await
222                    .map_err(|source| Error::BlockClaim {
223                        source,
224                        resource: block_hash.to_string(),
225                    })
226            }
227            .boxed()
228        })?
229        .get("claim_header_input_v2", |req, state| {
230            async move {
231                let block_hash: BuilderCommitment = req.blob_param("block_hash")?;
232                let view_number = req.integer_param("view_number")?;
233                let signature = try_extract_param(&req, "signature")?;
234                let sender = try_extract_param(&req, "sender")?;
235                let out = state
236                    .claim_block_header_input(&block_hash, view_number, sender, &signature)
237                    .await
238                    .map_err(|source| Error::BlockClaim {
239                        source,
240                        resource: block_hash.to_string(),
241                    });
242
243                out.map(|input| AvailableBlockHeaderInputV2::<Types> {
244                    fee_signature: input.fee_signature,
245                    sender: input.sender,
246                })
247            }
248            .boxed()
249        })?
250        .get("builder_address", |_req, state| {
251            async move { state.builder_address().await.map_err(|e| e.into()) }.boxed()
252        })?;
253    Ok(api)
254}
255
256pub fn submit_api<State, Types: NodeType, Ver: StaticVersionType + 'static>(
257    options: &Options,
258) -> Result<Api<State, Error, Ver>, ApiError>
259where
260    State: 'static + Send + Sync + ReadState,
261    <State as ReadState>::State: Send + Sync + AcceptsTxnSubmits<Types>,
262{
263    let mut api = load_api::<State, Error, Ver>(
264        options.api_path.as_ref(),
265        include_str!("../../api/v0_1/submit.toml"),
266        options.extensions.clone(),
267    )?;
268    api.with_version("0.0.1".parse().unwrap())
269        .at("submit_txn", |req: RequestParams, state| {
270            async move {
271                let tx = req
272                    .body_auto::<<Types as NodeType>::Transaction, Ver>(Ver::instance())
273                    .map_err(Error::TxnUnpack)?;
274                let hash = tx.commit();
275                state
276                    .read(|state| state.submit_txns(vec![tx]))
277                    .await
278                    .map_err(Error::TxnSubmit)?;
279                Ok(hash)
280            }
281            .boxed()
282        })?
283        .at("submit_batch", |req: RequestParams, state| {
284            async move {
285                let txns = req
286                    .body_auto::<Vec<<Types as NodeType>::Transaction>, Ver>(Ver::instance())
287                    .map_err(Error::TxnUnpack)?;
288                let hashes = txns.iter().map(|tx| tx.commit()).collect::<Vec<_>>();
289                state
290                    .read(|state| state.submit_txns(txns))
291                    .await
292                    .map_err(Error::TxnSubmit)?;
293                Ok(hashes)
294            }
295            .boxed()
296        })?
297        .get("get_status", |req: RequestParams, state| {
298            async move {
299                let tx = req
300                    .body_auto::<<Types as NodeType>::Transaction, Ver>(Ver::instance())
301                    .map_err(Error::TxnUnpack)?;
302                let hash = tx.commit();
303                state.txn_status(hash).await.map_err(Error::TxnStat)
304            }
305            .boxed()
306        })?;
307    Ok(api)
308}