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;
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 #[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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
55pub enum TransactionStatus {
56 Pending,
57 Sequenced { leaf: u64 },
58 Rejected { reason: String }, 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}