hotshot_builder_api/v0_1/
builder.rs1use 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;
16pub 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 #[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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
59pub enum TransactionStatus {
60 Pending,
61 Sequenced { leaf: u64 },
62 Rejected { reason: String }, 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}