espresso_types/v0/impls/
auction.rs

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    /// Proxy for `execute` method of each transaction variant.
27    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    /// Construct a new `BidTxBody`.
77    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    /// Sign Body and return a `BidTx`. This is the expected way to obtain a `BidTx`.
96    /// ```
97    /// # use espresso_types::FeeAccount;
98    /// # use espresso_types::v0_99::BidTxBody;
99    ///
100    /// BidTxBody::default().signed(&FeeAccount::test_key_pair()).unwrap();
101    /// ```
102    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    /// Get account submitting the bid
112    pub fn account(&self) -> FeeAccount {
113        self.account
114    }
115    /// Get amount of bid
116    pub fn amount(&self) -> FeeAmount {
117        self.bid_amount
118    }
119    /// get the view number
120    pub fn view(&self) -> ViewNumber {
121        self.view
122    }
123    /// Instantiate a `BidTxBody` containing the values of `self`
124    /// with a new `url` field.
125    pub fn with_url(self, url: Url) -> Self {
126        Self { url, ..self }
127    }
128
129    /// Get the cloned `url` field.
130    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)]
158/// Failure cases of transaction execution
159pub enum ExecutionError {
160    #[error("Invalid Signature")]
161    /// Transaction Signature could not be verified.
162    InvalidSignature,
163    #[error("Invalid Phase")]
164    /// Transaction submitted during incorrect Marketplace Phase
165    InvalidPhase,
166    #[error("FeeError: {0}")]
167    /// Insufficient funds or MerkleTree error.
168    FeeError(FeeError),
169    #[error("Could not resolve `ChainConfig`")]
170    /// Could not resolve `ChainConfig`.
171    UnresolvableChainConfig,
172    #[error("Bid recipient not set on `ChainConfig`")]
173    /// Bid Recipient is not set on `ChainConfig`
174    BidRecipientNotFound,
175}
176
177impl From<FeeError> for ExecutionError {
178    fn from(e: FeeError) -> Self {
179        Self::FeeError(e)
180    }
181}
182
183impl BidTx {
184    /// Execute `BidTx`.
185    ///   * verify signature
186    ///   * charge bid amount
187    ///   * charge gas
188    pub fn execute(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> {
189        self.verify()?;
190
191        // In JIT sequencer only receives winning bids. In AOT all
192        // bids are charged as received (losing bids are refunded). In
193        // any case we can charge the bids and gas during execution.
194        self.charge(state)?;
195
196        Ok(())
197    }
198    /// Charge Bid. Only winning bids are charged in JIT.
199    fn charge(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> {
200        // As the code is currently organized, I think chain_config
201        // will always be resolved here. But let's guard against the
202        // error in case code is shifted around in the future.
203        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        // Charge the bid amount
211        state
212            .charge_fee(FeeInfo::new(self.account(), self.amount()), recipient)
213            .map_err(ExecutionError::from)?;
214
215        // Charge the gas amount
216        state
217            .charge_fee(FeeInfo::new(self.account(), self.gas_price()), recipient)
218            .map_err(ExecutionError::from)?;
219
220        Ok(())
221    }
222    /// Cryptographic signature verification
223    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    /// Return the body of the transaction
231    pub fn body(self) -> BidTxBody {
232        self.body
233    }
234    /// get gas price
235    pub fn gas_price(&self) -> FeeAmount {
236        self.body.gas_price
237    }
238    /// get bid amount
239    pub fn amount(&self) -> FeeAmount {
240        self.body.bid_amount
241    }
242    /// get bid account
243    pub fn account(&self) -> FeeAccount {
244        self.body.account
245    }
246    /// get the view number
247    pub fn view(&self) -> ViewNumber {
248        self.body.view
249    }
250    /// Get the `url` field from the body.
251    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                        // Set a phantom type to make the compiler happy
279                        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    /// Construct a `SolverAuctionResults`
294    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    /// Get the view number for these auction results
306    pub fn view(&self) -> ViewNumber {
307        self.view_number
308    }
309    /// Get the winning bids of the auction
310    pub fn winning_bids(&self) -> &[BidTx] {
311        &self.winning_bids
312    }
313    /// Get the reserve bids of the auction
314    pub fn reserve_bids(&self) -> &[(NamespaceId, Url)] {
315        &self.reserve_bids
316    }
317    /// Empty results for the genesis view.
318    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    /// Get the urls to fetch bids from builders.
335    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)]
347/// Auction Results provider holding the Url of the solver in order to fetch auction results.
348pub 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    /// Fetch the auction results from the solver.
367    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] // TODO enable after upgrade to v3
401    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}