From 13ed70f33bb191dc62971c644869e3a104ebef6d Mon Sep 17 00:00:00 2001 From: tbro Date: Thu, 16 May 2024 14:53:27 -0500 Subject: [PATCH] FullNetworkTx implementation FullNetworkTx for MVP-0 Auctions * we are currently only concerned with `BidTx` * there is some very basic coverage of essential functionality --- sequencer/src/auction.rs | 264 +++++++++++++++++++++++++++++++++++++-- sequencer/src/header.rs | 16 +++ sequencer/src/state.rs | 72 ++++++++++- 3 files changed, 336 insertions(+), 16 deletions(-) diff --git a/sequencer/src/auction.rs b/sequencer/src/auction.rs index 8d68251421..34128d9c26 100644 --- a/sequencer/src/auction.rs +++ b/sequencer/src/auction.rs @@ -1,6 +1,42 @@ -use crate::{state::FeeAmount, NamespaceId}; -use hotshot_types::data::ViewNumber; -use std::num::NonZeroU64; +use crate::{ + eth_signature_key::EthKeyPair, + state::{FeeAccount, FeeAmount}, + NamespaceId, Transaction, ValidatedState, +}; +use ark_serialize::CanonicalSerialize; +use committable::{Commitment, Committable}; +use derivative::Derivative; +use ethers::types::Signature; +use hotshot::types::SignatureKey; +use hotshot_types::{data::ViewNumber, traits::signature_key::BuilderSignatureKey}; +use rand::RngCore; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + num::NonZeroU64, + str::FromStr, +}; +use url::Url; + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +struct SequencerKey; + +// for MVP-0(-1) (JIT) +// Sum of all sequencing fee match current check +// builder signature no longer includes payload +// new fee info flag (fee or bid) + +pub struct MarketplaceResults { + /// Slot these results are for + slot: Slot, + /// Bids that did not win, intially all bids are added to this, + /// and then the winning bids are removed + pending_bids: Vec, + /// Map of winning sequencer public keys to the bundle of namespaces they bid for + winner_map: HashMap>, + /// Whether refunds have been processed for this slot + refunds_processed: bool, +} // - needs to be configured in genesis block // - needs to be updatable @@ -12,12 +48,15 @@ struct AuctionConfig { } /// Uniquely identifies an auction for sequencing rights of namespaces in the network +#[derive(Clone, Copy)] struct AuctionId(u64); /// Uniquely identifies one auction phase for a specific auction +#[derive(Clone, Copy)] struct AuctionPhaseId(AuctionId, AuctionPhaseKind); /// Describes one auction phase for a specific auction +#[derive(Clone, Copy)] struct AuctionPhase { id: AuctionPhaseId, kind: AuctionPhaseKind, @@ -26,23 +65,180 @@ struct AuctionPhase { } /// Describes the 3 kinds of phases an active auction can be in +#[derive(Clone, Copy, Eq, PartialEq)] enum AuctionPhaseKind { Bid, Assign, Sequence, } -struct Signature; +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub struct Slot(u64); +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub enum FullNetworkTx { + Bid(BidTx), +} -/// A transaction to bid for the sequencing rights of a namespace +impl FullNetworkTx { + pub fn execute(&self, state: &ValidatedState) -> Result<(), (ExecutionError, FullNetworkTx)> { + dbg!("execute"); + match self { + Self::Bid(bid) => bid.execute(state), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] struct BidTx { - auction: AuctionId, - amount: FeeAmount, - namespace: NamespaceId, - nonce: Nonce, + body: BidTxBody, + signature: Signature, +} + +/// A transaction to bid for the sequencing rights of a namespace +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +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 What is the correct type? + /// The public key of this sequencer + public_key: SequencerKey, + /// 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 + slot: Slot, + /// The set of namespace ids the sequencer is bidding for + bundle: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +struct ShortBidTx { signature: Signature, + /// Account responsible for the signature + account: FeeAccount, + url: Url, +} + +// 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("slot", self.slot.0) + .var_size_field( + "bundle", + // TODO what is the correct way to serialize `Vec` + &bincode::serialize(&self.bundle.as_slice()).unwrap(), + ); + comm.finalize() + } +} + +impl Default for BidTxBody { + fn default() -> Self { + let message = ";)"; + let mut commitment = [0u8; 32]; + commitment[..message.len()].copy_from_slice(message.as_bytes()); + let (account, key) = FeeAccount::generated_from_seed_indexed([0; 32], 0); + let nsid = NamespaceId::from(999); + Self { + url: Url::from_str("htts://sequencer:3939/request_budle").unwrap(), + account, + public_key: SequencerKey, + gas_price: FeeAmount::default(), + bid_amount: FeeAmount::default(), + slot: Slot::default(), + bundle: vec![nsid], + } + } +} +impl Default for BidTx { + fn default() -> Self { + let body = BidTxBody::default(); + let commitment = body.commit(); + let (account, key) = FeeAccount::generated_from_seed_indexed([0; 32], 0); + let signature = FeeAccount::sign_builder_message(&key, &commitment.as_ref()).unwrap(); + Self { signature, body } + } } +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub enum ExecutionError { + InvalidSignature, + InvalidPhase, +} + +// TODO consider moving common functionality to trait. +impl BidTx { + /// Executes `BidTx`. + /// * verify signature + /// * charge fee + /// * store state + // The rational behind the `Err` is to provide not only what + // failed, but for which varient. 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: &ValidatedState) -> Result<(), (ExecutionError, FullNetworkTx)> { + if get_phase() != AuctionPhaseKind::Bid { + return Err(( + ExecutionError::InvalidPhase, + FullNetworkTx::Bid(self.clone()), + )); + } + self.verify() + .map_err(|e| (e, FullNetworkTx::Bid(self.clone())))?; + + // self.charge().map_err; + store_in_marketplace_state(); + + // TODO what do we return in good result? + Ok(()) + } + // fn charge(&self) {} + /// Verify signature + fn verify(&self) -> Result<(), ExecutionError> { + if !self + .body + .account + .validate_builder_signature(&self.signature, self.body.commit().as_ref()) + { + return Err(ExecutionError::InvalidSignature); + }; + + Ok(()) + } +} + +pub fn get_phase() -> AuctionPhaseKind { + AuctionPhaseKind::Bid +} + +fn store_in_marketplace_state() { + unimplemented!(); +} + +enum ExecutionResult { + Ok(T), + Err(E, K), +} + +// enum ExecutionResult { +// Bid(String), +// } + /// A solution to one auction of sequencing rights for namespaces struct AuctionResultTx { auction: AuctionId, @@ -51,12 +247,58 @@ struct AuctionResultTx { signature: Signature, } -// Not sure if needed +// Seems like sequencer could just check for refund flags on events so +// we probably don't need an explicit transaction type. struct BidRefundTx { nonce: Nonce, - txns_to_refund: Vec, + // I don't think we need the list b/c we have them in state + // txns_to_refund: Vec, signature: Signature, } /// Nonce for special (auction) transactions struct Nonce(u64); + +pub fn mock_full_network_txs() -> Vec { + let x = FullNetworkTx::Bid(BidTx::default()); + dbg!(&x); + vec![x] +} + +mod test { + use super::*; + + impl BidTx { + fn mock(account: FeeAccount, key: EthKeyPair) -> Self { + let body = BidTxBody::default(); + let commitment = body.commit(); + let signature = FeeAccount::sign_builder_message(&key, commitment.as_ref()).unwrap(); + Self { signature, body } + } + } + + #[test] + fn test_default_bidtx_body() { + let a = FeeAccount::default(); + + let message = ";)"; + let mut commitment = [0u8; 32]; + commitment[..message.len()].copy_from_slice(message.as_bytes()); + let key = FeeAccount::generated_from_seed_indexed([0; 32], 0).1; + let signature = FeeAccount::sign_builder_message(&key, &commitment).unwrap(); + let bid = BidTxBody::default(); + dbg!(&bid); + } + #[test] + fn test_mock_full_network_txs() { + let x = mock_full_network_txs(); + dbg!(&x); + } + #[test] + fn test_sign_and_verify_mock_bid() { + let account = FeeAccount::default(); + let key = FeeAccount::generated_from_seed_indexed([0; 32], 0).1; + let bidtx = BidTx::mock(account, key); + bidtx.verify().unwrap(); + } +} diff --git a/sequencer/src/header.rs b/sequencer/src/header.rs index 1ebb25b544..202f5202c1 100644 --- a/sequencer/src/header.rs +++ b/sequencer/src/header.rs @@ -1,4 +1,7 @@ +use std::collections::HashSet; + use crate::{ + auction::{mock_full_network_txs, FullNetworkTx, Slot}, block::NsTable, chain_config::ResolvableChainConfig, eth_signature_key::BuilderSignature, @@ -95,6 +98,11 @@ 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, + // pub 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 + // pub refund_bids: HashSet, } impl Committable for Header { @@ -133,6 +141,14 @@ impl Committable for Header { } impl Header { + // TODO move to BlockHeader + pub fn get_full_network_txs(&self) -> Vec { + // TODO unmock + mock_full_network_txs() + } + pub fn get_refund_bids(&self) -> HashSet { + unimplemented!(); + } #[allow(clippy::too_many_arguments)] fn from_info( payload_commitment: VidCommitment, diff --git a/sequencer/src/state.rs b/sequencer/src/state.rs index 0c515817e2..2b84c8a5a9 100644 --- a/sequencer/src/state.rs +++ b/sequencer/src/state.rs @@ -1,8 +1,14 @@ use crate::{ - api::data_source::CatchupDataSource, block::NsTableValidationError, catchup::SqlStateCatchup, - chain_config::BlockSize, chain_config::ResolvableChainConfig, eth_signature_key::EthKeyPair, - genesis::UpgradeType, persistence::ChainConfigPersistence, ChainConfig, Header, Leaf, - NodeState, SeqTypes, + api::data_source::CatchupDataSource, + auction::{ExecutionError, FullNetworkTx, MarketplaceResults, Slot}, + block::NsTableValidationError, + catchup::SqlStateCatchup, + chain_config::BlockSize, + chain_config::ResolvableChainConfig, + eth_signature_key::EthKeyPair, + genesis::UpgradeType, + persistence::ChainConfigPersistence, + ChainConfig, Header, Leaf, NodeState, SeqTypes, }; use anyhow::{bail, ensure, Context}; use ark_serialize::{ @@ -52,8 +58,8 @@ use sequencer_utils::{ impl_serde_from_string_or_integer, impl_to_fixed_bytes, ser::FromStringOrInteger, }; use serde::{Deserialize, Serialize}; -use std::sync::Arc; use std::time::Duration; +use std::{collections::HashMap, sync::Arc}; use std::{collections::HashSet, ops::Add, str::FromStr}; use thiserror::Error; use vbs::version::Version; @@ -77,6 +83,9 @@ pub struct ValidatedState { /// Fee Merkle Tree pub fee_merkle_tree: FeeMerkleTree, pub chain_config: ResolvableChainConfig, + // TODO + // /// Map of Marketplace Results. + // slot_map: HashMap, } #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] @@ -767,6 +776,25 @@ impl ValidatedState { let mut validated_state = apply_proposal(&validated_state, &mut delta, parent_leaf, l1_deposits); + // TODO we will need something similar to `charge_fee` but + // that charges `BidTx` to the account. We will also validate + // against bid phase time constraints. + // + // According to discussion we may receive multiple bids per + // header (up to configurable maximum), so this will likely be + // represented as `Vec` where `BidTx` holds account and + // amount. + // + // Note that different from `charge_fee` fee for the bid (sent + // to fee_recipient) which the bid itself will be held in an + // escrow account. + // charge_bids(&mut validated_state, bids_vec, fee_recipient, escrow_account); + + // or possibly we will call `charge_fee` twice, once as here + // and the 2nd will have escrow_account in recipient place + + // but where does the escrow account information come from? + // headers? state? charge_fee( &mut validated_state, &mut delta, @@ -940,6 +968,17 @@ impl HotShotState for ValidatedState { if parent_leaf.view_number().u64() % 10 == 0 { tracing::info!("validated and applied new header"); } + + if let Err((err, kind)) = apply_full_transactions( + &validated_state, + instance.chain_config, + proposed_header.get_full_network_txs(), + ) { + tracing::error!("Invalid Tx Error: {err:?}, kind: {kind:?}"); + // TODO review spec for conditions of BlockError + return Err(BlockError::InvalidBlockHeader); + } + Ok((validated_state, delta)) } /// Construct the state with the given block header. @@ -973,6 +1012,19 @@ impl HotShotState for ValidatedState { } } +fn apply_full_transactions( + validated_state: &ValidatedState, + chain_config: ChainConfig, + full_network_txs: Vec, +) -> Result<(), (ExecutionError, FullNetworkTx)> { + dbg!(&full_network_txs); + // proposed_header + // .get_full_network_txs() + full_network_txs + .iter() + .try_for_each(|tx| tx.execute(validated_state)) +} + // Required for TestableState #[cfg(any(test, feature = "testing"))] impl std::fmt::Display for ValidatedState { @@ -1467,6 +1519,8 @@ impl FeeAccountProof { #[cfg(test)] mod test { + use crate::auction::mock_full_network_txs; + use super::*; use async_compatibility_layer::logging::{setup_backtrace, setup_logging}; use hotshot_types::vid::vid_scheme; @@ -1632,6 +1686,14 @@ mod test { ); } + #[test] + fn test_apply_full_tx() { + let state = ValidatedState::default(); + let txs = mock_full_network_txs(); + let (err, bid) = apply_full_transactions(&state, ChainConfig::default(), txs).unwrap_err(); + assert_eq!(ExecutionError::InvalidSignature, err); + } + #[test] fn test_fee_amount_serde_json_as_decimal() { let amt = FeeAmount::from(123);