1use std::str::FromStr;
2
3use anyhow::Context;
4use async_trait::async_trait;
5use committable::{Commitment, Committable};
6use hotshot_types::{
7 data::ViewNumber,
8 traits::{
9 auction_results_provider::AuctionResultsProvider,
10 node_implementation::{ConsensusTime, HasUrls, NodeType},
11 signature_key::BuilderSignatureKey,
12 },
13};
14use thiserror::Error;
15use tide_disco::error::ServerError;
16use url::Url;
17
18use super::{state::ValidatedState, MarketplaceVersion};
19use crate::{
20 eth_signature_key::{EthKeyPair, SigningError},
21 v0_99::{BidTx, BidTxBody, FullNetworkTx, SolverAuctionResults},
22 FeeAccount, FeeAmount, FeeError, FeeInfo, NamespaceId,
23};
24
25impl FullNetworkTx {
26 pub fn execute(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> {
28 match self {
29 Self::Bid(bid) => bid.execute(state),
30 }
31 }
32}
33
34impl Committable for BidTx {
35 fn tag() -> String {
36 "BID_TX".to_string()
37 }
38
39 fn commit(&self) -> Commitment<Self> {
40 let comm = committable::RawCommitmentBuilder::new(&Self::tag())
41 .field("body", self.body.commit())
42 .fixed_size_field("signature", &self.signature.as_bytes());
43 comm.finalize()
44 }
45}
46
47impl Committable for BidTxBody {
48 fn tag() -> String {
49 "BID_TX_BODY".to_string()
50 }
51
52 fn commit(&self) -> Commitment<Self> {
53 let comm = committable::RawCommitmentBuilder::new(&Self::tag())
54 .fixed_size_field("account", &self.account.to_fixed_bytes())
55 .fixed_size_field("gas_price", &self.gas_price.to_fixed_bytes())
56 .fixed_size_field("bid_amount", &self.bid_amount.to_fixed_bytes())
57 .var_size_field("url", self.url.as_str().as_ref())
58 .u64_field("view", self.view.u64())
59 .array_field(
60 "namespaces",
61 &self
62 .namespaces
63 .iter()
64 .map(|e| {
65 committable::RawCommitmentBuilder::<BidTxBody>::new("namespace")
66 .u64(e.0)
67 .finalize()
68 })
69 .collect::<Vec<_>>(),
70 );
71 comm.finalize()
72 }
73}
74
75impl BidTxBody {
76 pub fn new(
78 account: FeeAccount,
79 bid: FeeAmount,
80 view: ViewNumber,
81 namespaces: Vec<NamespaceId>,
82 url: Url,
83 gas_price: FeeAmount,
84 ) -> Self {
85 Self {
86 account,
87 bid_amount: bid,
88 view,
89 namespaces,
90 url,
91 gas_price,
92 }
93 }
94
95 pub fn signed(self, key: &EthKeyPair) -> Result<BidTx, SigningError> {
103 let signature = FeeAccount::sign_builder_message(key, self.commit().as_ref())?;
104 let bid = BidTx {
105 body: self,
106 signature,
107 };
108 Ok(bid)
109 }
110
111 pub fn account(&self) -> FeeAccount {
113 self.account
114 }
115 pub fn amount(&self) -> FeeAmount {
117 self.bid_amount
118 }
119 pub fn view(&self) -> ViewNumber {
121 self.view
122 }
123 pub fn with_url(self, url: Url) -> Self {
126 Self { url, ..self }
127 }
128
129 fn url(&self) -> Url {
131 self.url.clone()
132 }
133}
134
135impl Default for BidTxBody {
136 fn default() -> Self {
137 let key = FeeAccount::test_key_pair();
138 let nsid = NamespaceId::from(999u64);
139 Self {
140 url: Url::from_str("https://sequencer:3939").unwrap(),
141 account: key.fee_account(),
142 gas_price: FeeAmount::default(),
143 bid_amount: FeeAmount::default(),
144 view: ViewNumber::genesis(),
145 namespaces: vec![nsid],
146 }
147 }
148}
149impl Default for BidTx {
150 fn default() -> Self {
151 BidTxBody::default()
152 .signed(&FeeAccount::test_key_pair())
153 .unwrap()
154 }
155}
156
157#[derive(Error, Debug, Eq, PartialEq)]
158pub enum ExecutionError {
160 #[error("Invalid Signature")]
161 InvalidSignature,
163 #[error("Invalid Phase")]
164 InvalidPhase,
166 #[error("FeeError: {0}")]
167 FeeError(FeeError),
169 #[error("Could not resolve `ChainConfig`")]
170 UnresolvableChainConfig,
172 #[error("Bid recipient not set on `ChainConfig`")]
173 BidRecipientNotFound,
175}
176
177impl From<FeeError> for ExecutionError {
178 fn from(e: FeeError) -> Self {
179 Self::FeeError(e)
180 }
181}
182
183impl BidTx {
184 pub fn execute(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> {
189 self.verify()?;
190
191 self.charge(state)?;
195
196 Ok(())
197 }
198 fn charge(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> {
200 let Some(chain_config) = state.chain_config.resolve() else {
204 return Err(ExecutionError::UnresolvableChainConfig);
205 };
206
207 let Some(recipient) = chain_config.bid_recipient else {
208 return Err(ExecutionError::BidRecipientNotFound);
209 };
210 state
212 .charge_fee(FeeInfo::new(self.account(), self.amount()), recipient)
213 .map_err(ExecutionError::from)?;
214
215 state
217 .charge_fee(FeeInfo::new(self.account(), self.gas_price()), recipient)
218 .map_err(ExecutionError::from)?;
219
220 Ok(())
221 }
222 fn verify(&self) -> Result<(), ExecutionError> {
224 self.body
225 .account
226 .validate_builder_signature(&self.signature, self.body.commit().as_ref())
227 .then_some(())
228 .ok_or(ExecutionError::InvalidSignature)
229 }
230 pub fn body(self) -> BidTxBody {
232 self.body
233 }
234 pub fn gas_price(&self) -> FeeAmount {
236 self.body.gas_price
237 }
238 pub fn amount(&self) -> FeeAmount {
240 self.body.bid_amount
241 }
242 pub fn account(&self) -> FeeAccount {
244 self.body.account
245 }
246 pub fn view(&self) -> ViewNumber {
248 self.body.view
249 }
250 pub fn url(&self) -> Url {
252 self.body.url()
253 }
254}
255
256impl Committable for SolverAuctionResults {
257 fn tag() -> String {
258 "SOLVER_AUCTION_RESULTS".to_string()
259 }
260
261 fn commit(&self) -> Commitment<Self> {
262 let comm = committable::RawCommitmentBuilder::new(&Self::tag())
263 .fixed_size_field("view_number", &self.view_number.commit().into())
264 .array_field(
265 "winning_bids",
266 &self
267 .winning_bids
268 .iter()
269 .map(Committable::commit)
270 .collect::<Vec<_>>(),
271 )
272 .array_field(
273 "reserve_bids",
274 &self
275 .reserve_bids
276 .iter()
277 .map(|(nsid, url)| {
278 committable::RawCommitmentBuilder::<SolverAuctionResults>::new(
280 "RESERVE_BID",
281 )
282 .u64(nsid.0)
283 .constant_str(url.as_str())
284 .finalize()
285 })
286 .collect::<Vec<_>>(),
287 );
288 comm.finalize()
289 }
290}
291
292impl SolverAuctionResults {
293 pub fn new(
295 view_number: ViewNumber,
296 winning_bids: Vec<BidTx>,
297 reserve_bids: Vec<(NamespaceId, Url)>,
298 ) -> Self {
299 Self {
300 view_number,
301 winning_bids,
302 reserve_bids,
303 }
304 }
305 pub fn view(&self) -> ViewNumber {
307 self.view_number
308 }
309 pub fn winning_bids(&self) -> &[BidTx] {
311 &self.winning_bids
312 }
313 pub fn reserve_bids(&self) -> &[(NamespaceId, Url)] {
315 &self.reserve_bids
316 }
317 pub fn genesis() -> Self {
319 Self {
320 view_number: ViewNumber::genesis(),
321 winning_bids: vec![],
322 reserve_bids: vec![],
323 }
324 }
325}
326
327impl Default for SolverAuctionResults {
328 fn default() -> Self {
329 Self::genesis()
330 }
331}
332
333impl HasUrls for SolverAuctionResults {
334 fn urls(&self) -> Vec<Url> {
336 self.winning_bids()
337 .iter()
338 .map(|bid| bid.url())
339 .chain(self.reserve_bids().iter().map(|bid| bid.1.clone()))
340 .collect()
341 }
342}
343
344type SurfClient = surf_disco::Client<ServerError, MarketplaceVersion>;
345
346#[derive(Debug, Clone, Eq, PartialEq, Hash)]
347pub struct SolverAuctionResultsProvider {
349 pub url: Url,
350 pub marketplace_path: String,
351 pub results_path: String,
352}
353
354impl Default for SolverAuctionResultsProvider {
355 fn default() -> Self {
356 Self {
357 url: Url::from_str("http://localhost:25000").unwrap(),
358 marketplace_path: "marketplace-solver/".into(),
359 results_path: "auction_results/".into(),
360 }
361 }
362}
363
364#[async_trait]
365impl<TYPES: NodeType> AuctionResultsProvider<TYPES> for SolverAuctionResultsProvider {
366 async fn fetch_auction_result(
368 &self,
369 view_number: TYPES::View,
370 ) -> anyhow::Result<TYPES::AuctionResult> {
371 let resp = SurfClient::new(
372 self.url
373 .join(&self.marketplace_path)
374 .context("Malformed solver URL")?,
375 )
376 .get::<TYPES::AuctionResult>(&format!("{}{}", self.results_path, *view_number))
377 .send()
378 .await?;
379 Ok(resp)
380 }
381}
382
383mod test {
384 use super::*;
385
386 impl BidTx {
387 pub fn mock(key: EthKeyPair) -> Self {
388 BidTxBody::default().signed(&key).unwrap()
389 }
390 }
391
392 #[test]
393 fn test_mock_bid_tx_sign_and_verify() {
394 let key = FeeAccount::test_key_pair();
395 let bidtx = BidTx::mock(key);
396 bidtx.verify().unwrap();
397 }
398
399 #[test]
400 #[ignore] fn test_mock_bid_tx_charge() {
402 let mut state = ValidatedState::default();
403 let key = FeeAccount::test_key_pair();
404 let bidtx = BidTx::mock(key);
405 bidtx.charge(&mut state).unwrap();
406 }
407
408 #[test]
409 fn test_bid_tx_construct() {
410 let key_pair = EthKeyPair::random();
411 BidTxBody::new(
412 key_pair.fee_account(),
413 FeeAmount::from(1),
414 ViewNumber::genesis(),
415 vec![NamespaceId::from(999u64)],
416 Url::from_str("https://my.builder:3131").unwrap(),
417 FeeAmount::default(),
418 )
419 .signed(&key_pair)
420 .unwrap()
421 .verify()
422 .unwrap();
423 }
424}