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