diff --git a/sequencer/src/genesis.rs b/sequencer/src/genesis.rs index 8ad111a66a..f189f91dd4 100644 --- a/sequencer/src/genesis.rs +++ b/sequencer/src/genesis.rs @@ -203,6 +203,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()) } ); @@ -267,6 +268,7 @@ mod test { max_block_size: 30000.into(), base_fee: 1.into(), fee_recipient: FeeAccount::default(), + bid_recipient: FeeAccount::default(), fee_contract: None, } ); 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/reference_tests.rs b/types/src/reference_tests.rs index b4ae97af51..a6bf64072d 100755 --- a/types/src/reference_tests.rs +++ b/types/src/reference_tests.rs @@ -99,6 +99,7 @@ fn reference_chain_config() -> ChainConfig { base_fee: 0.into(), fee_contract: Some(Default::default()), fee_recipient: Default::default(), + bid_recipient: Default::default(), } } diff --git a/types/src/v0/impls/auction.rs b/types/src/v0/impls/auction.rs new file mode 100644 index 0000000000..6e7009ad75 --- /dev/null +++ b/types/src/v0/impls/auction.rs @@ -0,0 +1,284 @@ +use crate::{ + eth_signature_key::{EthKeyPair, SigningError}, + v0_3::{BidTx, BidTxBody, FullNetworkTx}, + 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 std::str::FromStr; +use thiserror::Error; +use url::Url; + +impl FullNetworkTx { + pub fn execute( + &self, + state: &mut ValidatedState, + ) -> Result<(), (ExecutionError, FullNetworkTx)> { + match self { + Self::Bid(bid) => bid.execute(state), + } + } +} + +// TODO consider a committable derive macro +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, + ..Self::default() + } + } + + /// Sign `BidTxBody` and return the signature. + pub 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`. + /// ``` + /// 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 + } + /// 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(999u64); + 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 for ExecutionError { + fn from(e: FeeError) -> Self { + Self::FeeError(e) + } +} + +// TODO consider moving common functionality to trait. +impl BidTx { + /// Execute `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) -> Vec { + // 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(999u64)], + Url::from_str("https://sequencer:3131").unwrap(), + ) + .signed(&key_pair) + .unwrap() + .verify() + .unwrap(); + } +} diff --git a/types/src/v0/impls/chain_config.rs b/types/src/v0/impls/chain_config.rs index 754b2db5bd..de556b4712 100644 --- a/types/src/v0/impls/chain_config.rs +++ b/types/src/v0/impls/chain_config.rs @@ -85,6 +85,7 @@ impl Default for ChainConfig { base_fee: 0.into(), fee_contract: None, fee_recipient: Default::default(), + bid_recipient: Default::default(), } } } diff --git a/types/src/v0/impls/fee_info.rs b/types/src/v0/impls/fee_info.rs index 9eb463f44c..ffcd0e4f9c 100644 --- a/types/src/v0/impls/fee_info.rs +++ b/types/src/v0/impls/fee_info.rs @@ -1,7 +1,8 @@ -// use crate::SeqTypes; - -use std::str::FromStr; - +use crate::{ + eth_signature_key::EthKeyPair, v0_3::IterableFeeInfo, AccountQueryData, FeeAccount, + FeeAccountProof, FeeAmount, FeeInfo, FeeMerkleCommitment, FeeMerkleProof, FeeMerkleTree, + SeqTypes, +}; use anyhow::{bail, ensure, Context}; use ark_serialize::{ CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, @@ -24,14 +25,9 @@ use num_traits::CheckedSub; use sequencer_utils::{ impl_serde_from_string_or_integer, impl_to_fixed_bytes, ser::FromStringOrInteger, }; +use std::str::FromStr; use thiserror::Error; -use crate::{ - eth_signature_key::EthKeyPair, v0_3::IterableFeeInfo, AccountQueryData, FeeAccount, - FeeAccountProof, FeeAmount, FeeInfo, FeeMerkleCommitment, FeeMerkleProof, FeeMerkleTree, - SeqTypes, -}; - /// Possible charge fee failures #[derive(Error, Debug, Eq, PartialEq)] pub enum FeeError { diff --git a/types/src/v0/impls/header.rs b/types/src/v0/impls/header.rs index 6e12ab7b3a..1c0cb56816 100644 --- a/types/src/v0/impls/header.rs +++ b/types/src/v0/impls/header.rs @@ -28,7 +28,7 @@ use vbs::version::Version; use crate::{ v0::header::{EitherOrVersion, VersionedHeader}, v0_1, v0_2, - v0_3::{self, IterableFeeInfo}, + v0_3::{self, FullNetworkTx, IterableFeeInfo}, BlockMerkleCommitment, BlockSize, BuilderSignature, ChainConfig, FeeAccount, FeeAmount, FeeInfo, FeeMerkleCommitment, Header, L1BlockInfo, L1Snapshot, Leaf, NamespaceId, NodeState, NsTable, NsTableValidationError, ResolvableChainConfig, SeqTypes, UpgradeType, ValidatedState, @@ -128,6 +128,11 @@ impl Committable for Header { } impl Header { + pub fn get_full_network_txs(&self) -> Vec { + // TODO unmock + super::auction::mock_full_network_txs(None) + } + pub fn version(&self) -> Version { match self { Self::V1(_) => Version { major: 0, minor: 1 }, 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/impls/state.rs b/types/src/v0/impls/state.rs index e3ce2ded1d..aba7b07d94 100644 --- a/types/src/v0/impls/state.rs +++ b/types/src/v0/impls/state.rs @@ -24,12 +24,12 @@ use num_traits::CheckedSub; use thiserror::Error; use vbs::version::Version; -use super::{fee_info::FeeError, header::ProposalValidationError}; +use super::{auction::ExecutionError, fee_info::FeeError, header::ProposalValidationError}; use crate::{ - v0_3::IterableFeeInfo, BlockMerkleTree, ChainConfig, Delta, FeeAccount, FeeAmount, FeeInfo, - FeeMerkleTree, Header, Leaf, NodeState, NsTableValidationError, PayloadByteLen, - ResolvableChainConfig, SeqTypes, UpgradeType, ValidatedState, BLOCK_MERKLE_TREE_HEIGHT, - FEE_MERKLE_TREE_HEIGHT, + v0_3::{FullNetworkTx, IterableFeeInfo}, + BlockMerkleTree, ChainConfig, Delta, FeeAccount, FeeAmount, FeeInfo, FeeMerkleTree, Header, + Leaf, NodeState, NsTableValidationError, PayloadByteLen, ResolvableChainConfig, SeqTypes, + UpgradeType, ValidatedState, BLOCK_MERKLE_TREE_HEIGHT, FEE_MERKLE_TREE_HEIGHT, }; /// Possible builder validation failures @@ -417,6 +417,10 @@ impl ValidatedState { let mut validated_state = apply_proposal(&validated_state, &mut delta, parent_leaf, l1_deposits); + // TODO I guess we will need deltas for this? + apply_full_transactions(&mut validated_state, proposed_header.get_full_network_txs()) + .map_err(|e| anyhow::anyhow!("Error: {e:?}"))?; + charge_fee( &mut validated_state, &mut delta, @@ -479,6 +483,16 @@ impl ValidatedState { } } +fn apply_full_transactions( + validated_state: &mut ValidatedState, + full_network_txs: Vec, +) -> Result<(), (ExecutionError, FullNetworkTx)> { + dbg!(&full_network_txs); + full_network_txs + .iter() + .try_for_each(|tx| tx.execute(validated_state)) +} + pub async fn get_l1_deposits( instance: &NodeState, header: &Header, @@ -715,7 +729,24 @@ mod test { use sequencer_utils::ser::FromStringOrInteger; use super::*; - use crate::{BlockSize, FeeAccountProof, FeeMerkleProof}; + use crate::{ + v0::impls::auction::mock_full_network_txs, BlockSize, FeeAccountProof, FeeMerkleProof, + }; + + #[test] + fn test_apply_full_tx() { + let mut state = ValidatedState::default(); + let txs = mock_full_network_txs(None); + // Default key can be verified b/c it is the same that signs the mock tx + apply_full_transactions(&mut state, txs).unwrap(); + + // Tx will be invalid if it is signed by a different key than + // set in `account` field. + let key = FeeAccount::generated_from_seed_indexed([1; 32], 0).1; + let invalid = mock_full_network_txs(Some(key)); + let (err, _) = apply_full_transactions(&mut state, invalid).unwrap_err(); + assert_eq!(ExecutionError::InvalidSignature, err); + } #[test] fn test_fee_proofs() { diff --git a/types/src/v0/v0_1/chain_config.rs b/types/src/v0/v0_1/chain_config.rs index e74e36fda0..e63073209e 100644 --- a/types/src/v0/v0_1/chain_config.rs +++ b/types/src/v0/v0_1/chain_config.rs @@ -40,6 +40,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, } #[derive(Clone, Debug, Copy, PartialEq, Deserialize, Serialize, Eq, Hash)] diff --git a/types/src/v0/v0_3/auction.rs b/types/src/v0/v0_3/auction.rs new file mode 100644 index 0000000000..5d00e01978 --- /dev/null +++ b/types/src/v0/v0_3/auction.rs @@ -0,0 +1,44 @@ +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, + // TODO I think this will end up being a `FeeAccount` + /// The public key of this sequencer + pub(crate) public_key: FeeAccount, + /// 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, +} diff --git a/types/src/v0/v0_3/header.rs b/types/src/v0/v0_3/header.rs index f5efeb1f04..3063c73e0b 100644 --- a/types/src/v0/v0_3/header.rs +++ b/types/src/v0/v0_3/header.rs @@ -23,6 +23,10 @@ pub struct Header { pub(crate) fee_merkle_tree_root: FeeMerkleCommitment, pub(crate) fee_info: Vec, pub(crate) builder_signature: Vec, + // pub(crate) full_network_txs: Vec, + // /// 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 { diff --git a/types/src/v0/v0_3/mod.rs b/types/src/v0/v0_3/mod.rs index 4e9074a91d..cbb62ec412 100644 --- a/types/src/v0/v0_3/mod.rs +++ b/types/src/v0/v0_3/mod.rs @@ -16,8 +16,10 @@ pub use super::v0_1::{ pub const VERSION: Version = Version { major: 0, minor: 3 }; +mod auction; mod fee_info; mod header; +pub use auction::{BidTx, BidTxBody, FullNetworkTx}; pub use fee_info::IterableFeeInfo; pub use header::Header;