Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement BidTx #1633

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 313 additions & 0 deletions sequencer/src/auction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
use crate::{
eth_signature_key::{EthKeyPair, SigningError},
state::{FeeAccount, FeeAmount, FeeError, FeeInfo},
NamespaceId, ValidatedState,
};
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 serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;
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),
}

impl FullNetworkTx {
pub fn execute(
&self,
state: &mut ValidatedState,
) -> Result<(), (ExecutionError, FullNetworkTx)> {
match self {
Self::Bid(bid) => bid.execute(state),
}
}
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)]
pub struct BidTx {
body: BidTxBody,
signature: Signature,
}

/// A transaction to bid for the sequencing rights of a namespace
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)]
pub struct BidTxBody {
/// Account responsible for the signature
account: FeeAccount,
/// Fee to be sequenced in the network. Different than the bid_amount fee
// FULL_NETWORK_GAS * MINIMUM_GAS_PRICE
gas_price: FeeAmount,
/// The bid amount designated in Wei. This is different than
/// the sequencing fee (gas price) for this transaction
bid_amount: FeeAmount,
// TODO I think this will end up being a `FeeAccount`
/// The public key of this sequencer
public_key: FeeAccount,
/// The URL the HotShot leader will use to request a bundle
/// from this sequencer if they win the auction
url: Url,
/// The slot this bid is for
view: ViewNumber,
/// The set of namespace ids the sequencer is bidding for
namespaces: Vec<NamespaceId>,
}

// TODO consider a committable derive macro
impl Committable for BidTxBody {
fn tag() -> String {
"BID_TX".to_string()
}

fn commit(&self) -> Commitment<Self> {
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 {
pub fn new(
account: FeeAccount,
bid: FeeAmount,
view: ViewNumber,
namespaces: Vec<NamespaceId>,
) -> Self {
Self {
account,
bid_amount: bid,
view,
namespaces,
..Self::default()
}
}

/// Sign `BidTxBody` and return the signature.
pub fn sign(&self, key: &EthKeyPair) -> Result<Signature, SigningError> {
FeeAccount::sign_builder_message(key, self.commit().as_ref())
}
/// Sign Body and return a `BidTx`
pub fn signed(self, key: &EthKeyPair) -> Result<BidTx, SigningError> {
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
}
/// Update `url` field on a previously instantiated `BidTxBody`.
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(999);
Self {
// TODO url will be builder_url, needs to be passed in from somewhere
url: Url::from_str("https://sequencer:3939").unwrap(),
account: key.fee_account(),
public_key: FeeAccount::default(),
gas_price: FeeAmount::default(),
bid_amount: FeeAmount::default(),
view: ViewNumber::genesis(),
namespaces: vec![nsid],
}
}
}
impl Default for BidTx {
fn default() -> Self {
let body = BidTxBody::default();
let key = FeeAccount::test_key_pair();
let signature = FeeAccount::sign_builder_message(&key, body.commit().as_ref()).unwrap();
Self { signature, body }
}
}

#[derive(Error, Debug, Eq, PartialEq)]
/// Failure cases of transaction execution
pub enum ExecutionError {
#[error("Invalid Signature")]
InvalidSignature,
#[error("Invalid Phase")]
InvalidPhase,
#[error("FeeError: {0}")]
FeeError(FeeError),
#[error("Could not resolve `ChainConfig`")]
UnresolvableChainConfig,
}

impl From<FeeError> for ExecutionError {
fn from(e: FeeError) -> Self {
Self::FeeError(e)
}
}

// TODO consider moving common functionality to trait.
impl BidTx {
/// Executes `BidTx`.
/// * verify signature
/// * charge fee
// The rational behind the `Err` is to provide not only what
// failed, but for which variant. The entire Tx is probably
// overkill, but we can narrow down how much we want to know about
// Failed Tx in the future. Maybe we just want its name.
pub fn execute(
&self,
state: &mut ValidatedState,
) -> Result<(), (ExecutionError, FullNetworkTx)> {
self.verify()
.map_err(|e| (e, FullNetworkTx::Bid(self.clone())))?;

// 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)
.map_err(|e| (e, FullNetworkTx::Bid(self.clone())))?;

// TODO what do we return in good result?
Ok(())
}
/// Charge Bid. Only winning bids are charged in JIT (I think).
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);
};

let recipient = chain_config.bid_recipient;
// Charge the bid amount
state
.charge_fee(FeeInfo::new(self.account(), self.amount()), recipient)
.map_err(ExecutionError::from)?;

// TODO are gas and bid funded to same recipient? Possibly
// gas would be funded to recipient sequencing
// fee recipient?
// 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
}
/// Update `url` field on a previously instantiated `BidTxBody`.
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 amount
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()
}
}

pub fn mock_full_network_txs(key: Option<EthKeyPair>) -> Vec<FullNetworkTx> {
// if no key is supplied, use `test_key_pair`. Since default `BidTxBody` is
// signed with `test_key_pair`, it will verify successfully
let key = key.unwrap_or_else(FeeAccount::test_key_pair);
vec![FullNetworkTx::Bid(BidTx::mock(key))]
}

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(999)],
)
.signed(&key_pair)
.unwrap()
.verify()
.unwrap();
}
}
4 changes: 4 additions & 0 deletions sequencer/src/chain_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ pub struct ChainConfig {
/// regardless of whether or not their is a `fee_contract` deployed. Once deployed, the fee
/// contract can decide what to do with tokens locked in this account in Espresso.
pub fee_recipient: FeeAccount,

/// Account that receives sequencing bids.
pub bid_recipient: FeeAccount,
}

impl Default for ChainConfig {
Expand All @@ -120,6 +123,7 @@ impl Default for ChainConfig {
base_fee: 0.into(),
fee_contract: None,
fee_recipient: Default::default(),
bid_recipient: Default::default(),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions sequencer/src/eth_signature_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
Expand Down
2 changes: 2 additions & 0 deletions sequencer/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ mod test {
max_block_size: 30000.into(),
base_fee: 1.into(),
fee_recipient: FeeAccount::default(),
bid_recipient: FeeAccount::default(),
fee_contract: Some(Address::default())
}
);
Expand Down Expand Up @@ -340,6 +341,7 @@ mod test {
max_block_size: 30000.into(),
base_fee: 1.into(),
fee_recipient: FeeAccount::default(),
bid_recipient: FeeAccount::default(),
fee_contract: None,
}
);
Expand Down
12 changes: 12 additions & 0 deletions sequencer/src/header.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::collections::HashSet;

use crate::{
auction::{mock_full_network_txs, FullNetworkTx},
block::NsTable,
chain_config::ResolvableChainConfig,
eth_signature_key::BuilderSignature,
Expand Down Expand Up @@ -95,6 +98,10 @@ pub struct Header {
/// that `fee_info` is correct without relying on the signature. Thus, this signature is not
/// included in the header commitment.
pub builder_signature: Option<BuilderSignature>,
// pub full_network_txs: Vec<FullNetworkTx>,
// /// refund flag set at the beginning of new slots
// /// In extreme cases, more than one slot may need to be refunded,
// /// hence this data structure
}

impl Committable for Header {
Expand Down Expand Up @@ -133,6 +140,11 @@ impl Committable for Header {
}

impl Header {
// TODO move to BlockHeader
pub fn get_full_network_txs(&self) -> Vec<FullNetworkTx> {
// TODO unmock
mock_full_network_txs(None)
}
#[allow(clippy::too_many_arguments)]
fn from_info(
payload_commitment: VidCommitment,
Expand Down
1 change: 1 addition & 0 deletions sequencer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod api;
pub mod auction;
pub mod block;
pub mod catchup;
mod chain_config;
Expand Down
Loading
Loading