diff --git a/types/src/eth_signature_key.rs b/types/src/eth_signature_key.rs index 6843fd0b89..d50068e488 100644 --- a/types/src/eth_signature_key.rs +++ b/types/src/eth_signature_key.rs @@ -48,6 +48,9 @@ impl EthKeyPair { let signing_key: &SigningKey = derived_priv_key.as_ref(); Ok(signing_key.clone().into()) } + pub fn random() -> EthKeyPair { + SigningKey::random(&mut rand::thread_rng()).into() + } pub fn fee_account(&self) -> FeeAccount { self.fee_account diff --git a/types/src/v0/impls/auction.rs b/types/src/v0/impls/auction.rs new file mode 100644 index 0000000000..ac4e03a35b --- /dev/null +++ b/types/src/v0/impls/auction.rs @@ -0,0 +1,271 @@ +use crate::{ + eth_signature_key::{EthKeyPair, SigningError}, + v0_1::ValidatedState, + v0_3::{BidTx, BidTxBody, FullNetworkTx}, + FeeAccount, FeeAmount, FeeError, FeeInfo, NamespaceId, +}; +use committable::{Commitment, Committable}; +use ethers::types::Signature; +use hotshot_types::{ + data::ViewNumber, + traits::{ + auction_results_provider::HasUrl, node_implementation::ConsensusTime, + signature_key::BuilderSignatureKey, + }, +}; +use std::str::FromStr; +use thiserror::Error; +use url::Url; + +impl FullNetworkTx { + /// Proxy for `execute` method of each transaction variant. + pub fn execute(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> { + match self { + Self::Bid(bid) => bid.execute(state), + } + } +} + +impl Committable for BidTxBody { + fn tag() -> String { + "BID_TX".to_string() + } + + fn commit(&self) -> Commitment { + let comm = committable::RawCommitmentBuilder::new(&Self::tag()) + .fixed_size_field("account", &self.account.to_fixed_bytes()) + .fixed_size_field("gas_price", &self.gas_price.to_fixed_bytes()) + .fixed_size_field("bid_amount", &self.bid_amount.to_fixed_bytes()) + .var_size_field("url", self.url.as_str().as_ref()) + .u64_field("view", self.view.u64()) + .var_size_field("namespaces", &bincode::serialize(&self.namespaces).unwrap()); + comm.finalize() + } +} + +impl BidTxBody { + /// Construct a new `BidTxBody`. + pub fn new( + account: FeeAccount, + bid: FeeAmount, + view: ViewNumber, + namespaces: Vec, + url: Url, + ) -> Self { + Self { + account, + bid_amount: bid, + view, + namespaces, + url, + // TODO gas_price will come from config probably, but we + // can use any value for first round of integration + ..Self::default() + } + } + + /// Sign `BidTxBody` and return the signature. + fn sign(&self, key: &EthKeyPair) -> Result { + FeeAccount::sign_builder_message(key, self.commit().as_ref()) + } + /// Sign Body and return a `BidTx`. This is the expected way to obtain a `BidTx`. + /// ``` + /// # use espresso_types::FeeAccount; + /// # use espresso_types::v0_3::BidTxBody; + /// + /// let key = FeeAccount::test_key_pair(); + /// BidTxBody::default().signed(&key).unwrap(); + /// ``` + pub fn signed(self, key: &EthKeyPair) -> Result { + let signature = self.sign(key)?; + let bid = BidTx { + body: self, + signature, + }; + Ok(bid) + } + + /// Get account submitting the bid + pub fn account(&self) -> FeeAccount { + self.account + } + /// Get amount of bid + pub fn amount(&self) -> FeeAmount { + self.bid_amount + } + /// Instantiate a `BidTxBody` containing the values of `self` + /// with a new `url` field. + pub fn with_url(self, url: Url) -> Self { + Self { url, ..self } + } +} + +impl Default for BidTxBody { + fn default() -> Self { + let key = FeeAccount::test_key_pair(); + let nsid = NamespaceId::from(999u64); + Self { + url: Url::from_str("https://sequencer:3939").unwrap(), + account: key.fee_account(), + gas_price: FeeAmount::default(), + bid_amount: FeeAmount::default(), + view: ViewNumber::genesis(), + namespaces: vec![nsid], + } + } +} +impl Default for BidTx { + fn default() -> Self { + BidTxBody::default() + .signed(&FeeAccount::test_key_pair()) + .unwrap() + } +} + +#[derive(Error, Debug, Eq, PartialEq)] +/// Failure cases of transaction execution +pub enum ExecutionError { + #[error("Invalid Signature")] + /// Transaction Signature could not be verified. + InvalidSignature, + #[error("Invalid Phase")] + /// Transaction submitted during incorrect Marketplace Phase + InvalidPhase, + #[error("FeeError: {0}")] + /// Insufficient funds or MerkleTree error. + FeeError(FeeError), + #[error("Could not resolve `ChainConfig`")] + /// Could not resolve `ChainConfig`. + UnresolvableChainConfig, +} + +impl From for ExecutionError { + fn from(e: FeeError) -> Self { + Self::FeeError(e) + } +} + +impl BidTx { + /// Execute `BidTx`. + /// * verify signature + /// * charge bid amount + /// * charge gas + pub fn execute(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> { + self.verify()?; + + // In JIT sequencer only receives winning bids. In AOT all + // bids are charged as received (losing bids are refunded). In + // any case we can charge the bids and gas during execution. + self.charge(state)?; + + Ok(()) + } + /// Charge Bid. Only winning bids are charged in JIT. + fn charge(&self, state: &mut ValidatedState) -> Result<(), ExecutionError> { + // As the code is currently organized, I think chain_config + // will always be resolved here. But let's guard against the + // error in case code is shifted around in the future. + let Some(chain_config) = state.chain_config.resolve() else { + return Err(ExecutionError::UnresolvableChainConfig); + }; + + // TODO change to `bid_recipient` when this logic is finally enabled + let recipient = chain_config.fee_recipient; + // Charge the bid amount + state + .charge_fee(FeeInfo::new(self.account(), self.amount()), recipient) + .map_err(ExecutionError::from)?; + + // Charge the the gas amount + state + .charge_fee(FeeInfo::new(self.account(), self.gas_price()), recipient) + .map_err(ExecutionError::from)?; + + Ok(()) + } + /// Cryptographic signature verification + fn verify(&self) -> Result<(), ExecutionError> { + self.body + .account + .validate_builder_signature(&self.signature, self.body.commit().as_ref()) + .then_some(()) + .ok_or(ExecutionError::InvalidSignature) + } + /// Return the body of the transaction + pub fn body(self) -> BidTxBody { + self.body + } + /// Instantiate a `BidTx` containing the values of `self` + /// with a new `url` field on `body`. + pub fn with_url(self, url: Url) -> Self { + let body = self.body.with_url(url); + Self { body, ..self } + } + /// get gas price + pub fn gas_price(&self) -> FeeAmount { + self.body.gas_price + } + /// get bid amount + pub fn amount(&self) -> FeeAmount { + self.body.bid_amount + } + /// get bid account + pub fn account(&self) -> FeeAccount { + self.body.account + } +} + +impl HasUrl for BidTx { + /// Get the `url` field from the body. + fn url(&self) -> Url { + self.body.url() + } +} + +impl HasUrl for BidTxBody { + /// Get the cloned `url` field. + fn url(&self) -> Url { + self.url.clone() + } +} + +mod test { + use super::*; + + impl BidTx { + pub fn mock(key: EthKeyPair) -> Self { + BidTxBody::default().signed(&key).unwrap() + } + } + + #[test] + fn test_mock_bid_tx_sign_and_verify() { + let key = FeeAccount::test_key_pair(); + let bidtx = BidTx::mock(key); + bidtx.verify().unwrap(); + } + + #[test] + fn test_mock_bid_tx_charge() { + let mut state = ValidatedState::default(); + let key = FeeAccount::test_key_pair(); + let bidtx = BidTx::mock(key); + bidtx.charge(&mut state).unwrap(); + } + + #[test] + fn test_bid_tx_construct() { + let key_pair = EthKeyPair::random(); + BidTxBody::new( + key_pair.fee_account(), + FeeAmount::from(1), + ViewNumber::genesis(), + vec![NamespaceId::from(999u64)], + Url::from_str("https://sequencer:3131").unwrap(), + ) + .signed(&key_pair) + .unwrap() + .verify() + .unwrap(); + } +} diff --git a/types/src/v0/impls/mod.rs b/types/src/v0/impls/mod.rs index 3940fb0097..91aad85957 100644 --- a/types/src/v0/impls/mod.rs +++ b/types/src/v0/impls/mod.rs @@ -1,5 +1,6 @@ pub use super::*; +mod auction; mod block; mod chain_config; mod fee_info; diff --git a/types/src/v0/v0_3/auction.rs b/types/src/v0/v0_3/auction.rs new file mode 100644 index 0000000000..a44e368006 --- /dev/null +++ b/types/src/v0/v0_3/auction.rs @@ -0,0 +1,52 @@ +use crate::{FeeAccount, FeeAmount, NamespaceId}; +use ethers::types::Signature; +use hotshot_types::data::ViewNumber; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +/// Wrapper enum for Full Network Transactions. Each transaction type +/// will be a variant of this enum. +pub enum FullNetworkTx { + Bid(BidTx), +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +/// A transaction to bid for the sequencing rights of a namespace. It +/// is the `signed` form of `BidTxBody`. Expected usage is *build* +/// it by calling `signed` on `BidTxBody`. +pub struct BidTx { + pub(crate) body: BidTxBody, + pub(crate) signature: Signature, +} + +/// A transaction body holding data required for bid submission. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub struct BidTxBody { + /// Account responsible for the signature + pub(crate) account: FeeAccount, + /// Fee to be sequenced in the network. Different than the bid_amount fee + // FULL_NETWORK_GAS * MINIMUM_GAS_PRICE + pub(crate) gas_price: FeeAmount, + /// The bid amount designated in Wei. This is different than + /// the sequencing fee (gas price) for this transaction + pub(crate) bid_amount: FeeAmount, + /// The URL the HotShot leader will use to request a bundle + /// from this sequencer if they win the auction + pub(crate) url: Url, + /// The slot this bid is for + pub(crate) view: ViewNumber, + /// The set of namespace ids the sequencer is bidding for + pub(crate) namespaces: Vec, +} + +/// The results of an Auction +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub struct AuctionResults { + /// view number the results are for + pub(crate) view_number: ViewNumber, + /// A list of the bid txs that won + pub(crate) winning_bids: Vec, + /// A list of reserve sequencers being used + pub(crate) reserve_bids: Vec<(NamespaceId, Url)>, +} diff --git a/types/src/v0/v0_3/mod.rs b/types/src/v0/v0_3/mod.rs index a3f38283c8..f8a6ef1b6a 100644 --- a/types/src/v0/v0_3/mod.rs +++ b/types/src/v0/v0_3/mod.rs @@ -17,6 +17,8 @@ pub use super::v0_1::{ pub const VERSION: Version = Version { major: 0, minor: 3 }; +mod auction; mod header; +pub use auction::{AuctionResults, BidTx, BidTxBody, FullNetworkTx}; pub use header::Header;