From e952a1e091f3ade57ed06403f9f7e8ad4b800f34 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 23 Apr 2024 14:58:51 -0600 Subject: [PATCH 01/14] impl custom payload builder attributes --- Cargo.lock | 3 +- Cargo.toml | 3 +- mev-build-rs/Cargo.toml | 3 +- mev-build-rs/src/auctioneer.rs | 3 +- mev-build-rs/src/bidder.rs | 7 +- mev-build-rs/src/builder.rs | 35 +- mev-build-rs/src/lib.rs | 1 + mev-build-rs/src/node.rs | 71 +++ mev-build-rs/src/payload/builder.rs | 410 ++++++++++++++++++ .../src/payload/builder_attributes.rs | 116 +++++ mev-build-rs/src/payload/mod.rs | 2 + mev-build-rs/src/payload/service_builder.rs | 13 +- mev-build-rs/src/service.rs | 14 +- 13 files changed, 645 insertions(+), 36 deletions(-) create mode 100644 mev-build-rs/src/node.rs create mode 100644 mev-build-rs/src/payload/builder.rs create mode 100644 mev-build-rs/src/payload/builder_attributes.rs diff --git a/Cargo.lock b/Cargo.lock index d35c4f16..5e6dcec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4713,9 +4713,10 @@ dependencies = [ "reth", "reth-basic-payload-builder", "reth-db", - "reth-ethereum-payload-builder", + "reth-evm-ethereum", "reth-node-ethereum", "serde", + "sha2 0.10.8", "thiserror", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 833c2831..82439b11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,11 +20,12 @@ beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus", reth = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } reth-db = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } +reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } -reth-ethereum-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } eyre = "0.6.8" futures-util = "0.3.30" +sha2 = "0.10.8" [patch.crates-io] c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", tag = "v1.0.1" } diff --git a/mev-build-rs/Cargo.toml b/mev-build-rs/Cargo.toml index f1ffbb83..caae21dc 100644 --- a/mev-build-rs/Cargo.toml +++ b/mev-build-rs/Cargo.toml @@ -25,9 +25,10 @@ mev-rs = { path = "../mev-rs" } reth = { workspace = true } reth-db = { workspace = true } reth-node-ethereum = { workspace = true } +reth-evm-ethereum = { workspace = true } reth-basic-payload-builder = { workspace = true } -reth-ethereum-payload-builder = { workspace = true } +sha2 = { workspace = true } ethers = "2.0" eyre = { workspace = true } clap = { version = "4.1.4", features = ["derive", "env"] } diff --git a/mev-build-rs/src/auctioneer.rs b/mev-build-rs/src/auctioneer.rs index 3c071514..60f08266 100644 --- a/mev-build-rs/src/auctioneer.rs +++ b/mev-build-rs/src/auctioneer.rs @@ -18,6 +18,7 @@ use mev_rs::{ BlindedBlockRelayer, Relay, }; use reth::{ + api::PayloadBuilderAttributes, payload::{EthBuiltPayload, PayloadId}, tasks::TaskExecutor, }; @@ -39,7 +40,7 @@ fn prepare_submission( ) -> Result { let message = BidTrace { slot: auction_context.slot, - parent_hash: to_bytes32(auction_context.attributes.parent), + parent_hash: to_bytes32(auction_context.attributes.inner.parent), block_hash: to_bytes32(payload.block().hash()), builder_public_key: public_key.clone(), proposer_public_key: auction_context.proposer.public_key.clone(), diff --git a/mev-build-rs/src/bidder.rs b/mev-build-rs/src/bidder.rs index 1cf2041c..42096ad4 100644 --- a/mev-build-rs/src/bidder.rs +++ b/mev-build-rs/src/bidder.rs @@ -1,16 +1,17 @@ use crate::{ auction_schedule::{Proposer, RelaySet}, + payload::builder_attributes::BuilderPayloadBuilderAttributes, utils::payload_job::duration_until, }; use ethereum_consensus::primitives::Slot; -use reth::payload::{EthPayloadBuilderAttributes, PayloadId}; +use reth::{api::PayloadBuilderAttributes, payload::PayloadId}; use std::time::Duration; use tokio::time::sleep; #[derive(Debug)] pub struct AuctionContext { pub slot: Slot, - pub attributes: EthPayloadBuilderAttributes, + pub attributes: BuilderPayloadBuilderAttributes, pub proposer: Proposer, pub relays: RelaySet, } @@ -34,7 +35,7 @@ impl DeadlineBidder { } pub async fn make_bid(&self, auction: &AuctionContext) -> BidRequest { - let target = duration_until(auction.attributes.timestamp); + let target = duration_until(auction.attributes.timestamp()); let duration = target.checked_sub(self.deadline).unwrap_or_default(); sleep(duration).await; BidRequest::Ready(auction.attributes.payload_id()) diff --git a/mev-build-rs/src/builder.rs b/mev-build-rs/src/builder.rs index d1d493d8..e6f01ccd 100644 --- a/mev-build-rs/src/builder.rs +++ b/mev-build-rs/src/builder.rs @@ -2,6 +2,7 @@ use crate::{ auction_schedule::{Proposals, Proposer}, auctioneer::Message as AuctioneerMessage, bidder::AuctionContext, + payload::builder_attributes::BuilderPayloadBuilderAttributes, Error, }; use ethereum_consensus::{ @@ -9,10 +10,7 @@ use ethereum_consensus::{ }; use reth::{ api::{EngineTypes, PayloadBuilderAttributes}, - payload::{ - EthBuiltPayload, EthPayloadBuilderAttributes, Events, PayloadBuilderHandle, PayloadId, - PayloadStore, - }, + payload::{EthBuiltPayload, Events, PayloadBuilderHandle, PayloadId, PayloadStore}, primitives::Address, rpc::{ compat::engine::convert_withdrawal_to_standalone_withdraw, types::engine::PayloadAttributes, @@ -29,15 +27,16 @@ use tokio_stream::StreamExt; use tracing::warn; fn make_attributes_for_proposer( - attributes: &EthPayloadBuilderAttributes, + attributes: &BuilderPayloadBuilderAttributes, builder_fee_recipient: Address, -) -> EthPayloadBuilderAttributes { +) -> BuilderPayloadBuilderAttributes { // TODO: extend attributes with gas limit and proposer fee recipient - let withdrawals = if attributes.withdrawals.is_empty() { + let withdrawals = if attributes.inner.withdrawals.is_empty() { None } else { Some( attributes + .inner .withdrawals .iter() .cloned() @@ -46,14 +45,14 @@ fn make_attributes_for_proposer( ) }; let payload_attributes = PayloadAttributes { - timestamp: attributes.timestamp, - prev_randao: attributes.prev_randao, + timestamp: attributes.inner.timestamp, + prev_randao: attributes.inner.prev_randao, suggested_fee_recipient: builder_fee_recipient, withdrawals, - parent_beacon_block_root: attributes.parent_beacon_block_root, + parent_beacon_block_root: attributes.inner.parent_beacon_block_root, }; - EthPayloadBuilderAttributes::try_new(attributes.parent, payload_attributes) + BuilderPayloadBuilderAttributes::try_new(attributes.inner.parent, payload_attributes) .expect("conversion currently always succeeds") } @@ -73,7 +72,7 @@ pub struct Config { pub struct Builder< Engine: EngineTypes< - PayloadBuilderAttributes = EthPayloadBuilderAttributes, + PayloadBuilderAttributes = BuilderPayloadBuilderAttributes, BuiltPayload = EthBuiltPayload, >, > { @@ -89,7 +88,7 @@ pub struct Builder< impl< Engine: EngineTypes< - PayloadBuilderAttributes = EthPayloadBuilderAttributes, + PayloadBuilderAttributes = BuilderPayloadBuilderAttributes, BuiltPayload = EthBuiltPayload, > + 'static, > Builder @@ -119,7 +118,7 @@ impl< pub async fn process_proposals( &self, slot: Slot, - attributes: EthPayloadBuilderAttributes, + attributes: BuilderPayloadBuilderAttributes, proposals: Option, ) -> Result, Error> { let mut new_auctions = vec![]; @@ -144,8 +143,8 @@ impl< async fn start_build( &self, _proposer: &Proposer, - attributes: &EthPayloadBuilderAttributes, - ) -> Option { + attributes: &BuilderPayloadBuilderAttributes, + ) -> Option { match self.payload_builder.new_payload(attributes.clone()).await { Ok(payload_id) => { let attributes_payload_id = attributes.payload_id(); @@ -171,14 +170,14 @@ impl< }); } - async fn on_payload_attributes(&self, attributes: EthPayloadBuilderAttributes) { + async fn on_payload_attributes(&self, attributes: BuilderPayloadBuilderAttributes) { // NOTE: the payload builder currently makes a job for the incoming `attributes`. // We want to customize the building logic and so we cancel this first job unconditionally. self.terminate_job(attributes.payload_id()); // TODO: move slot calc to auctioneer? let slot = convert_timestamp_to_slot( - attributes.timestamp, + attributes.timestamp(), self.genesis_time, self.context.seconds_per_slot, ) diff --git a/mev-build-rs/src/lib.rs b/mev-build-rs/src/lib.rs index 144fa8ab..f53d3a18 100644 --- a/mev-build-rs/src/lib.rs +++ b/mev-build-rs/src/lib.rs @@ -3,6 +3,7 @@ mod auctioneer; mod bidder; mod builder; mod error; +mod node; mod payload; mod service; mod utils; diff --git a/mev-build-rs/src/node.rs b/mev-build-rs/src/node.rs new file mode 100644 index 00000000..9006ce62 --- /dev/null +++ b/mev-build-rs/src/node.rs @@ -0,0 +1,71 @@ +//! Customized types for the builder to configuring reth + +use crate::payload::{ + builder_attributes::BuilderPayloadBuilderAttributes, service_builder::PayloadServiceBuilder, +}; +use reth::{ + api::{ + validate_version_specific_fields, EngineApiMessageVersion, EngineObjectValidationError, + EngineTypes, FullNodeTypes, PayloadOrAttributes, + }, + builder::{components::ComponentsBuilder, NodeTypes}, + payload::EthBuiltPayload, + primitives::ChainSpec, + rpc::types::{ + engine::{ + ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, + PayloadAttributes as EthPayloadAttributes, + }, + ExecutionPayloadV1, + }, +}; +use reth_evm_ethereum::EthEvmConfig; +use reth_node_ethereum::node::{EthereumNetworkBuilder, EthereumPoolBuilder}; + +#[derive(Debug, Default, Clone, Copy)] +pub struct BuilderNode; + +impl BuilderNode { + /// Returns a [ComponentsBuilder] configured for a regular Ethereum node. + pub fn components( + ) -> ComponentsBuilder + where + Node: FullNodeTypes, + { + ComponentsBuilder::default() + .node_types::() + .pool(EthereumPoolBuilder::default()) + .payload(PayloadServiceBuilder::default()) + .network(EthereumNetworkBuilder::default()) + } +} + +impl NodeTypes for BuilderNode { + type Primitives = (); + type Engine = BuilderEngineTypes; + type Evm = EthEvmConfig; + + fn evm_config(&self) -> Self::Evm { + EthEvmConfig::default() + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct BuilderEngineTypes; + +impl EngineTypes for BuilderEngineTypes { + type PayloadAttributes = EthPayloadAttributes; + type PayloadBuilderAttributes = BuilderPayloadBuilderAttributes; + type BuiltPayload = EthBuiltPayload; + type ExecutionPayloadV1 = ExecutionPayloadV1; + type ExecutionPayloadV2 = ExecutionPayloadEnvelopeV2; + type ExecutionPayloadV3 = ExecutionPayloadEnvelopeV3; + + fn validate_version_specific_fields( + chain_spec: &ChainSpec, + version: EngineApiMessageVersion, + payload_or_attrs: PayloadOrAttributes<'_, Self::PayloadAttributes>, + ) -> Result<(), EngineObjectValidationError> { + validate_version_specific_fields(chain_spec, version, payload_or_attrs) + } +} diff --git a/mev-build-rs/src/payload/builder.rs b/mev-build-rs/src/payload/builder.rs new file mode 100644 index 00000000..d3ec258b --- /dev/null +++ b/mev-build-rs/src/payload/builder.rs @@ -0,0 +1,410 @@ +use crate::payload::builder_attributes::BuilderPayloadBuilderAttributes; +use reth::{ + api::PayloadBuilderAttributes, + payload::{error::PayloadBuilderError, EthBuiltPayload}, + primitives::{ + constants::{ + eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, + }, + eip4844::calculate_excess_blob_gas, + proofs, + revm::env::tx_env_with_recovered, + Block, Header, IntoRecoveredTransaction, Receipt, Receipts, EMPTY_OMMER_ROOT_HASH, U256, + }, + providers::{BundleStateWithReceipts, StateProviderFactory}, + revm::{ + self, + database::StateProviderDatabase, + db::states::bundle_state::BundleRetention, + primitives::{EVMError, EnvWithHandlerCfg, InvalidTransaction, ResultAndState}, + DatabaseCommit, State, + }, + transaction_pool::{BestTransactionsAttributes, TransactionPool}, +}; +use reth_basic_payload_builder::{ + commit_withdrawals, is_better_payload, pre_block_beacon_root_contract_call, BuildArguments, + BuildOutcome, PayloadConfig, WithdrawalsOutcome, +}; +use tracing::{debug, trace, warn}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PayloadBuilder; + +impl reth_basic_payload_builder::PayloadBuilder for PayloadBuilder +where + Client: StateProviderFactory, + Pool: TransactionPool, +{ + type Attributes = BuilderPayloadBuilderAttributes; + type BuiltPayload = EthBuiltPayload; + + fn try_build( + &self, + args: BuildArguments, + ) -> Result, PayloadBuilderError> { + default_ethereum_payload_builder(args) + } + + fn build_empty_payload( + client: &Client, + config: PayloadConfig, + ) -> Result { + let extra_data = config.extra_data(); + let PayloadConfig { + initialized_block_env, + parent_block, + attributes, + chain_spec, + initialized_cfg, + .. + } = config; + + debug!(target: "payload_builder", parent_hash = ?parent_block.hash(), parent_number = parent_block.number, "building empty payload"); + + let state = client.state_by_block_hash(parent_block.hash()).map_err(|err| { + warn!(target: "payload_builder", parent_hash=%parent_block.hash(), %err, "failed to get state for empty payload"); + err + })?; + let mut db = State::builder() + .with_database_boxed(Box::new(StateProviderDatabase::new(&state))) + .with_bundle_update() + .build(); + + let base_fee = initialized_block_env.basefee.to::(); + let block_number = initialized_block_env.number.to::(); + let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + + // apply eip-4788 pre block contract call + pre_block_beacon_root_contract_call( + &mut db, + &chain_spec, + block_number, + &initialized_cfg, + &initialized_block_env, + &attributes, + ).map_err(|err| { + warn!(target: "payload_builder", parent_hash=%parent_block.hash(), %err, "failed to apply beacon root contract call for empty payload"); + err + })?; + + let WithdrawalsOutcome { withdrawals_root, withdrawals } = + commit_withdrawals(&mut db, &chain_spec, attributes.timestamp(), attributes.withdrawals().clone()).map_err(|err| { + warn!(target: "payload_builder", parent_hash=%parent_block.hash(), %err, "failed to commit withdrawals for empty payload"); + err + })?; + + // merge all transitions into bundle state, this would apply the withdrawal balance + // changes and 4788 contract call + db.merge_transitions(BundleRetention::PlainState); + + // calculate the state root + let bundle_state = db.take_bundle(); + let state_root = state.state_root(&bundle_state).map_err(|err| { + warn!(target: "payload_builder", parent_hash=%parent_block.hash(), %err, "failed to calculate state root for empty payload"); + err + })?; + + let mut excess_blob_gas = None; + let mut blob_gas_used = None; + + if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp()) { + excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_block.timestamp) { + let parent_excess_blob_gas = parent_block.excess_blob_gas.unwrap_or_default(); + let parent_blob_gas_used = parent_block.blob_gas_used.unwrap_or_default(); + Some(calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used)) + } else { + // for the first post-fork block, both parent.blob_gas_used and + // parent.excess_blob_gas are evaluated as 0 + Some(calculate_excess_blob_gas(0, 0)) + }; + + blob_gas_used = Some(0); + } + + let header = Header { + parent_hash: parent_block.hash(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: initialized_block_env.coinbase, + state_root, + transactions_root: EMPTY_TRANSACTIONS, + withdrawals_root, + receipts_root: EMPTY_RECEIPTS, + logs_bloom: Default::default(), + timestamp: attributes.timestamp(), + mix_hash: attributes.prev_randao(), + nonce: BEACON_NONCE, + base_fee_per_gas: Some(base_fee), + number: parent_block.number + 1, + gas_limit: block_gas_limit, + difficulty: U256::ZERO, + gas_used: 0, + extra_data, + blob_gas_used, + excess_blob_gas, + parent_beacon_block_root: attributes.parent_beacon_block_root(), + }; + + let block = Block { header, body: vec![], ommers: vec![], withdrawals }; + let sealed_block = block.seal_slow(); + + Ok(EthBuiltPayload::new(attributes.payload_id(), sealed_block, U256::ZERO)) + } +} + +/// Constructs an Ethereum transaction payload using the best transactions from the pool. +/// +/// Given build arguments including an Ethereum client, transaction pool, +/// and configuration, this function creates a transaction payload. Returns +/// a result indicating success with the payload or an error in case of failure. +#[inline] +pub fn default_ethereum_payload_builder( + args: BuildArguments, +) -> Result, PayloadBuilderError> +where + Client: StateProviderFactory, + Pool: TransactionPool, +{ + let BuildArguments { client, pool, mut cached_reads, config, cancel, best_payload } = args; + + let state_provider = client.state_by_block_hash(config.parent_block.hash())?; + let state = StateProviderDatabase::new(&state_provider); + let mut db = + State::builder().with_database_ref(cached_reads.as_db(&state)).with_bundle_update().build(); + let extra_data = config.extra_data(); + let PayloadConfig { + initialized_block_env, + initialized_cfg, + parent_block, + attributes, + chain_spec, + .. + } = config; + + debug!(target: "payload_builder", id=%attributes.payload_id(), parent_hash = ?parent_block.hash(), parent_number = parent_block.number, "building new payload"); + let mut cumulative_gas_used = 0; + let mut sum_blob_gas_used = 0; + let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + let base_fee = initialized_block_env.basefee.to::(); + + let mut executed_txs = Vec::new(); + + let mut best_txs = pool.best_transactions_with_attributes(BestTransactionsAttributes::new( + base_fee, + initialized_block_env.get_blob_gasprice().map(|gasprice| gasprice as u64), + )); + + let mut total_fees = U256::ZERO; + + let block_number = initialized_block_env.number.to::(); + + // apply eip-4788 pre block contract call + pre_block_beacon_root_contract_call( + &mut db, + &chain_spec, + block_number, + &initialized_cfg, + &initialized_block_env, + &attributes, + )?; + + let mut receipts = Vec::new(); + while let Some(pool_tx) = best_txs.next() { + // ensure we still have capacity for this transaction + if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit { + // we can't fit this transaction into the block, so we need to mark it as invalid + // which also removes all dependent transaction from the iterator before we can + // continue + best_txs.mark_invalid(&pool_tx); + continue + } + + // check if the job was cancelled, if so we can exit early + if cancel.is_cancelled() { + return Ok(BuildOutcome::Cancelled) + } + + // convert tx to a signed transaction + let tx = pool_tx.to_recovered_transaction(); + + // There's only limited amount of blob space available per block, so we need to check if + // the EIP-4844 can still fit in the block + if let Some(blob_tx) = tx.transaction.as_eip4844() { + let tx_blob_gas = blob_tx.blob_gas(); + if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK { + // we can't fit this _blob_ transaction into the block, so we mark it as + // invalid, which removes its dependent transactions from + // the iterator. This is similar to the gas limit condition + // for regular transactions above. + trace!(target: "payload_builder", tx=?tx.hash, ?sum_blob_gas_used, ?tx_blob_gas, "skipping blob transaction because it would exceed the max data gas per block"); + best_txs.mark_invalid(&pool_tx); + continue + } + } + + // Configure the environment for the block. + let mut evm = revm::Evm::builder() + .with_db(&mut db) + .with_env_with_handler_cfg(EnvWithHandlerCfg::new_with_cfg_env( + initialized_cfg.clone(), + initialized_block_env.clone(), + tx_env_with_recovered(&tx), + )) + .build(); + + let ResultAndState { result, state } = match evm.transact() { + Ok(res) => res, + Err(err) => { + match err { + EVMError::Transaction(err) => { + if matches!(err, InvalidTransaction::NonceTooLow { .. }) { + // if the nonce is too low, we can skip this transaction + trace!(target: "payload_builder", %err, ?tx, "skipping nonce too low transaction"); + } else { + // if the transaction is invalid, we can skip it and all of its + // descendants + trace!(target: "payload_builder", %err, ?tx, "skipping invalid transaction and its descendants"); + best_txs.mark_invalid(&pool_tx); + } + + continue + } + err => { + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(err)) + } + } + } + }; + // drop evm so db is released. + drop(evm); + // commit changes + db.commit(state); + + // add to the total blob gas used if the transaction successfully executed + if let Some(blob_tx) = tx.transaction.as_eip4844() { + let tx_blob_gas = blob_tx.blob_gas(); + sum_blob_gas_used += tx_blob_gas; + + // if we've reached the max data gas per block, we can skip blob txs entirely + if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK { + best_txs.skip_blobs(); + } + } + + let gas_used = result.gas_used(); + + // add gas used by the transaction to cumulative gas used, before creating the receipt + cumulative_gas_used += gas_used; + + // Push transaction changeset and calculate header bloom filter for receipt. + #[allow(clippy::needless_update)] // side-effect of optimism fields + receipts.push(Some(Receipt { + tx_type: tx.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.into_logs().into_iter().map(Into::into).collect(), + ..Default::default() + })); + + // update add to total fees + let miner_fee = tx + .effective_tip_per_gas(Some(base_fee)) + .expect("fee is always valid; execution succeeded"); + total_fees += U256::from(miner_fee) * U256::from(gas_used); + + // append transaction to the list of executed transactions + executed_txs.push(tx.into_signed()); + } + + // check if we have a better block + if !is_better_payload(best_payload.as_ref(), total_fees) { + // can skip building the block + return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) + } + + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( + &mut db, + &chain_spec, + attributes.timestamp(), + attributes.withdrawals().clone(), + )?; + + // merge all transitions into bundle state, this would apply the withdrawal balance changes + // and 4788 contract call + db.merge_transitions(BundleRetention::PlainState); + + let bundle = BundleStateWithReceipts::new( + db.take_bundle(), + Receipts::from_vec(vec![receipts]), + block_number, + ); + let receipts_root = bundle.receipts_root_slow(block_number).expect("Number is in range"); + let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); + + // calculate the state root + let state_root = state_provider.state_root(bundle.state())?; + + // create the block header + let transactions_root = proofs::calculate_transaction_root(&executed_txs); + + // initialize empty blob sidecars at first. If cancun is active then this will + let mut blob_sidecars = Vec::new(); + let mut excess_blob_gas = None; + let mut blob_gas_used = None; + + // only determine cancun fields when active + if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp()) { + // grab the blob sidecars from the executed txs + blob_sidecars = pool.get_all_blobs_exact( + executed_txs.iter().filter(|tx| tx.is_eip4844()).map(|tx| tx.hash).collect(), + )?; + + excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_block.timestamp) { + let parent_excess_blob_gas = parent_block.excess_blob_gas.unwrap_or_default(); + let parent_blob_gas_used = parent_block.blob_gas_used.unwrap_or_default(); + Some(calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used)) + } else { + // for the first post-fork block, both parent.blob_gas_used and + // parent.excess_blob_gas are evaluated as 0 + Some(calculate_excess_blob_gas(0, 0)) + }; + + blob_gas_used = Some(sum_blob_gas_used); + } + + let header = Header { + parent_hash: parent_block.hash(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: initialized_block_env.coinbase, + state_root, + transactions_root, + receipts_root, + withdrawals_root, + logs_bloom, + timestamp: attributes.timestamp(), + mix_hash: attributes.prev_randao(), + nonce: BEACON_NONCE, + base_fee_per_gas: Some(base_fee), + number: parent_block.number + 1, + gas_limit: block_gas_limit, + difficulty: U256::ZERO, + gas_used: cumulative_gas_used, + extra_data, + parent_beacon_block_root: attributes.parent_beacon_block_root(), + blob_gas_used, + excess_blob_gas, + }; + + // seal the block + let block = Block { header, body: executed_txs, ommers: vec![], withdrawals }; + + let sealed_block = block.seal_slow(); + debug!(target: "payload_builder", ?sealed_block, "sealed built block"); + + let mut payload = EthBuiltPayload::new(attributes.payload_id(), sealed_block, total_fees); + + // extend the payload with the blob sidecars from the executed txs + payload.extend_sidecars(blob_sidecars); + + Ok(BuildOutcome::Better { payload, cached_reads }) +} diff --git a/mev-build-rs/src/payload/builder_attributes.rs b/mev-build-rs/src/payload/builder_attributes.rs new file mode 100644 index 00000000..fd966b50 --- /dev/null +++ b/mev-build-rs/src/payload/builder_attributes.rs @@ -0,0 +1,116 @@ +use reth::{ + api::PayloadBuilderAttributes, + payload::{EthPayloadBuilderAttributes, PayloadId}, + primitives::{ + alloy_primitives::private::alloy_rlp::Encodable, + revm_primitives::{BlockEnv, CfgEnvWithHandlerCfg}, + Address, ChainSpec, Header, Withdrawals, B256, + }, + rpc::{ + compat::engine::convert_standalone_withdraw_to_withdrawal, types::engine::PayloadAttributes, + }, +}; +use std::convert::Infallible; + +pub fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId { + use sha2::Digest; + let mut hasher = sha2::Sha256::new(); + hasher.update(parent.as_slice()); + hasher.update(&attributes.timestamp.to_be_bytes()[..]); + hasher.update(attributes.prev_randao.as_slice()); + hasher.update(attributes.suggested_fee_recipient.as_slice()); + if let Some(withdrawals) = &attributes.withdrawals { + let mut buf = Vec::new(); + withdrawals.encode(&mut buf); + hasher.update(buf); + } + + if let Some(parent_beacon_block) = attributes.parent_beacon_block_root { + hasher.update(parent_beacon_block); + } + + let out = hasher.finalize(); + PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length")) +} + +#[derive(Clone, Debug)] +pub struct BuilderPayloadBuilderAttributes { + pub inner: EthPayloadBuilderAttributes, +} + +impl BuilderPayloadBuilderAttributes { + pub fn new(parent: B256, attributes: PayloadAttributes) -> Self { + let id = payload_id(&parent, &attributes); + + let withdraw = attributes.withdrawals.map(|withdrawals| { + Withdrawals::new( + withdrawals + .into_iter() + .map(convert_standalone_withdraw_to_withdrawal) // Removed the parentheses here + .collect(), + ) + }); + + let inner = EthPayloadBuilderAttributes { + id, + parent, + timestamp: attributes.timestamp, + suggested_fee_recipient: attributes.suggested_fee_recipient, + prev_randao: attributes.prev_randao, + withdrawals: withdraw.unwrap_or_default(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + }; + Self { inner } + } +} + +unsafe impl Send for BuilderPayloadBuilderAttributes {} +unsafe impl Sync for BuilderPayloadBuilderAttributes {} + +impl PayloadBuilderAttributes for BuilderPayloadBuilderAttributes { + type RpcPayloadAttributes = PayloadAttributes; + type Error = Infallible; + + fn try_new( + parent: B256, + rpc_payload_attributes: Self::RpcPayloadAttributes, + ) -> Result { + Ok(Self::new(parent, rpc_payload_attributes)) + } + + fn payload_id(&self) -> PayloadId { + self.inner.payload_id() + } + + fn parent(&self) -> B256 { + self.inner.parent + } + + fn timestamp(&self) -> u64 { + self.inner.timestamp + } + + fn parent_beacon_block_root(&self) -> Option { + self.inner.parent_beacon_block_root + } + + fn suggested_fee_recipient(&self) -> Address { + self.inner.suggested_fee_recipient + } + + fn prev_randao(&self) -> B256 { + self.inner.prev_randao + } + + fn withdrawals(&self) -> &Withdrawals { + &self.inner.withdrawals + } + + fn cfg_and_block_env( + &self, + chain_spec: &ChainSpec, + parent: &Header, + ) -> (CfgEnvWithHandlerCfg, BlockEnv) { + self.inner.cfg_and_block_env(chain_spec, parent) + } +} diff --git a/mev-build-rs/src/payload/mod.rs b/mev-build-rs/src/payload/mod.rs index 21887ba3..4a97eb3c 100644 --- a/mev-build-rs/src/payload/mod.rs +++ b/mev-build-rs/src/payload/mod.rs @@ -1,5 +1,7 @@ //! Implementation based off of `paradigmxyz/reth` payload builder +pub mod builder; +pub mod builder_attributes; pub mod job; pub mod job_generator; pub mod service_builder; diff --git a/mev-build-rs/src/payload/service_builder.rs b/mev-build-rs/src/payload/service_builder.rs index 25630c46..9ff291ce 100644 --- a/mev-build-rs/src/payload/service_builder.rs +++ b/mev-build-rs/src/payload/service_builder.rs @@ -1,4 +1,10 @@ -use crate::payload::job_generator::{PayloadJobGenerator, PayloadJobGeneratorConfig}; +use crate::{ + node::BuilderEngineTypes, + payload::{ + builder::PayloadBuilder, + job_generator::{PayloadJobGenerator, PayloadJobGeneratorConfig}, + }, +}; use reth::{ builder::{node::FullNodeTypes, BuilderContext}, cli::config::PayloadBuilderConfig, @@ -6,7 +12,6 @@ use reth::{ providers::CanonStateSubscriptions, transaction_pool::TransactionPool, }; -use reth_node_ethereum::EthEngineTypes; #[derive(Debug, Clone, Copy, Default)] pub struct PayloadServiceBuilder; @@ -14,7 +19,7 @@ pub struct PayloadServiceBuilder; impl reth::builder::components::PayloadServiceBuilder for PayloadServiceBuilder where - Node: FullNodeTypes, + Node: FullNodeTypes, Pool: TransactionPool + Unpin + 'static, { async fn spawn_payload_service( @@ -38,7 +43,7 @@ where ctx.task_executor().clone(), payload_job_config, ctx.chain_spec().clone(), - reth_ethereum_payload_builder::EthereumPayloadBuilder::default(), + PayloadBuilder::default(), ); let (payload_service, payload_builder) = diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index 806d3018..4eb90948 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -1,7 +1,8 @@ use crate::{ auctioneer::{Auctioneer, Config as AuctioneerConfig}, builder::{Builder, Config as BuilderConfig}, - payload::service_builder::PayloadServiceBuilder, + node::BuilderNode, + payload::builder_attributes::BuilderPayloadBuilderAttributes, }; use ethereum_consensus::{ clock::SystemClock, networks::Network, primitives::Epoch, state_transition::Context, @@ -10,12 +11,11 @@ use mev_rs::{get_genesis_time, Error}; use reth::{ api::EngineTypes, builder::{InitState, WithLaunchContext}, - payload::{EthBuiltPayload, EthPayloadBuilderAttributes, PayloadBuilderHandle}, + payload::{EthBuiltPayload, PayloadBuilderHandle}, primitives::Bytes, tasks::TaskExecutor, }; use reth_db::DatabaseEnv; -use reth_node_ethereum::EthereumNode; use serde::Deserialize; use std::sync::Arc; use tokio::{ @@ -55,7 +55,7 @@ pub struct Config { pub struct Services< Engine: EngineTypes< - PayloadBuilderAttributes = EthPayloadBuilderAttributes, + PayloadBuilderAttributes = BuilderPayloadBuilderAttributes, BuiltPayload = EthBuiltPayload, >, > { @@ -68,7 +68,7 @@ pub struct Services< pub async fn construct< Engine: EngineTypes< - PayloadBuilderAttributes = EthPayloadBuilderAttributes, + PayloadBuilderAttributes = BuilderPayloadBuilderAttributes, BuiltPayload = EthBuiltPayload, > + 'static, >( @@ -123,8 +123,8 @@ pub async fn launch( // TODO: ability to just run reth let handle = node_builder - .with_types(EthereumNode::default()) - .with_components(EthereumNode::components().payload(PayloadServiceBuilder)) + .with_types(BuilderNode::default()) + .with_components(BuilderNode::components()) .launch() .await?; From 77153faad137955470d8128c70516d56dd014b21 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 23 Apr 2024 19:29:05 -0600 Subject: [PATCH 02/14] map new proposals to payload jobs --- example.config.toml | 5 +- mev-build-rs/src/auction_schedule.rs | 7 +- mev-build-rs/src/auctioneer.rs | 39 ++++---- mev-build-rs/src/builder.rs | 90 +++++-------------- .../src/payload/builder_attributes.rs | 55 ++++++++++-- mev-build-rs/src/payload/job.rs | 13 ++- mev-build-rs/src/payload/job_generator.rs | 19 ++-- mev-build-rs/src/payload/service_builder.rs | 14 ++- mev-build-rs/src/service.rs | 12 +-- mev-build-rs/src/utils/compat.rs | 2 +- 10 files changed, 140 insertions(+), 116 deletions(-) diff --git a/example.config.toml b/example.config.toml index 916e53f3..f2c89b53 100644 --- a/example.config.toml +++ b/example.config.toml @@ -19,8 +19,6 @@ accepted_builders = [ ] [builder] -# extra data to write into built execution payload -extra_data = "0x68656C6C6F20776F726C640A" # "hello world" # wallet seed for builder to author payment transactions execution_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" # number of milliseconds to submit bids ahead of the target slot @@ -39,4 +37,7 @@ relays = [ "https://0x845bd072b7cd566f02faeb0a4033ce9399e42839ced64e8b2adcfc859ed1e8e1a5a293336a49feac6d9a5edb779be53a@boost-relay-sepolia.flashbots.net", ] [builder.builder] +# address to collect transaction fees fee_recipient = "0x0000000000000000000000000000000000000000" +# [optional] extra data to write into built execution payload +extra_data = "0x68656C6C6F20776F726C640A" # "hello world" diff --git a/mev-build-rs/src/auction_schedule.rs b/mev-build-rs/src/auction_schedule.rs index 22bd3a3d..84594142 100644 --- a/mev-build-rs/src/auction_schedule.rs +++ b/mev-build-rs/src/auction_schedule.rs @@ -1,5 +1,6 @@ -use ethereum_consensus::primitives::{BlsPublicKey, ExecutionAddress, Slot}; +use ethereum_consensus::primitives::{BlsPublicKey, Slot}; use mev_rs::{types::ProposerSchedule, Relay}; +use reth::primitives::Address; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -11,7 +12,7 @@ pub type Proposals = HashMap; #[derive(Debug, Default, Hash, PartialEq, Eq)] pub struct Proposer { pub public_key: BlsPublicKey, - pub fee_recipient: ExecutionAddress, + pub fee_recipient: Address, pub gas_limit: u64, } @@ -37,7 +38,7 @@ impl AuctionSchedule { let registration = &entry.entry.message; let proposer = Proposer { public_key: registration.public_key.clone(), - fee_recipient: registration.fee_recipient.clone(), + fee_recipient: Address::from_slice(registration.fee_recipient.as_ref()), gas_limit: registration.gas_limit, }; let relays = slot.entry(proposer).or_default(); diff --git a/mev-build-rs/src/auctioneer.rs b/mev-build-rs/src/auctioneer.rs index 60f08266..b6f9f96c 100644 --- a/mev-build-rs/src/auctioneer.rs +++ b/mev-build-rs/src/auctioneer.rs @@ -3,7 +3,7 @@ use crate::{ bidder::{AuctionContext, BidRequest, DeadlineBidder}, builder::{KeepAlive, Message as BuilderMessage}, service::ClockMessage, - utils::compat::{to_bytes32, to_execution_payload}, + utils::compat::{to_bytes20, to_bytes32, to_execution_payload}, Error, }; use ethereum_consensus::{ @@ -44,7 +44,7 @@ fn prepare_submission( block_hash: to_bytes32(payload.block().hash()), builder_public_key: public_key.clone(), proposer_public_key: auction_context.proposer.public_key.clone(), - proposer_fee_recipient: auction_context.proposer.fee_recipient.clone(), + proposer_fee_recipient: to_bytes20(auction_context.proposer.fee_recipient), gas_limit: payload.block().gas_limit, gas_used: payload.block().gas_used, value: payload.fees(), @@ -114,7 +114,8 @@ impl Auctioneer { self.auction_schedule.take_matching_proposals(slot) } - fn process_new_auction(&mut self, payload_id: PayloadId, auction: AuctionContext) { + fn process_new_auction(&mut self, auction: AuctionContext) { + let payload_id = auction.attributes.payload_id(); self.open_auctions.insert(payload_id, Arc::new(auction)); let auction = self.open_auctions.get(&payload_id).unwrap().clone(); @@ -137,8 +138,7 @@ impl Auctioneer { fn process_new_auctions(&mut self, auctions: Vec) { for auction in auctions { - let payload_id = auction.attributes.payload_id(); - self.process_new_auction(payload_id, auction); + self.process_new_auction(auction); } } @@ -163,19 +163,26 @@ impl Auctioneer { self.open_auctions.retain(|_, auction| auction.slot >= slot); } - async fn dispatch_payload(&self, payload: EthBuiltPayload) { + async fn submit_payload(&self, payload: EthBuiltPayload) { let auction = self.open_auctions.get(&payload.id()).expect("has auction"); - let signed_submission = prepare_submission( + match prepare_submission( payload, &self.config.secret_key, &self.config.public_key, auction, &self.context, - ) - .expect("can prepare bid"); - let relays = &auction.relays; - for relay in relays { - relay.submit_bid(&signed_submission).await.expect("was ok"); + ) { + Ok(signed_submission) => { + let relays = &auction.relays; + for relay in relays { + if let Err(err) = relay.submit_bid(&signed_submission).await { + warn!(%err, ?relay, slot = auction.slot, "could not submit payload"); + } + } + } + Err(err) => { + warn!(%err, slot = auction.slot, "could not prepare submission") + } } } @@ -186,12 +193,8 @@ impl Auctioneer { let proposals = self.take_proposals(slot); tx.send(proposals).expect("can send"); } - NewAuctions(auctions) => { - self.process_new_auctions(auctions); - } - BuiltPayload(payload) => { - self.dispatch_payload(payload).await; - } + NewAuctions(auctions) => self.process_new_auctions(auctions), + BuiltPayload(payload) => self.submit_payload(payload).await, } } diff --git a/mev-build-rs/src/builder.rs b/mev-build-rs/src/builder.rs index e6f01ccd..739412a3 100644 --- a/mev-build-rs/src/builder.rs +++ b/mev-build-rs/src/builder.rs @@ -2,7 +2,7 @@ use crate::{ auction_schedule::{Proposals, Proposer}, auctioneer::Message as AuctioneerMessage, bidder::AuctionContext, - payload::builder_attributes::BuilderPayloadBuilderAttributes, + payload::builder_attributes::{BuilderPayloadBuilderAttributes, ProposalAttributes}, Error, }; use ethereum_consensus::{ @@ -11,11 +11,7 @@ use ethereum_consensus::{ use reth::{ api::{EngineTypes, PayloadBuilderAttributes}, payload::{EthBuiltPayload, Events, PayloadBuilderHandle, PayloadId, PayloadStore}, - primitives::Address, - rpc::{ - compat::engine::convert_withdrawal_to_standalone_withdraw, types::engine::PayloadAttributes, - }, - tasks::TaskExecutor, + primitives::{Address, Bytes}, }; use serde::Deserialize; use std::sync::Arc; @@ -24,36 +20,21 @@ use tokio::sync::{ oneshot, }; use tokio_stream::StreamExt; -use tracing::warn; +use tracing::{error, warn}; fn make_attributes_for_proposer( attributes: &BuilderPayloadBuilderAttributes, builder_fee_recipient: Address, + proposer: &Proposer, ) -> BuilderPayloadBuilderAttributes { - // TODO: extend attributes with gas limit and proposer fee recipient - let withdrawals = if attributes.inner.withdrawals.is_empty() { - None - } else { - Some( - attributes - .inner - .withdrawals - .iter() - .cloned() - .map(convert_withdrawal_to_standalone_withdraw) - .collect(), - ) - }; - let payload_attributes = PayloadAttributes { - timestamp: attributes.inner.timestamp, - prev_randao: attributes.inner.prev_randao, - suggested_fee_recipient: builder_fee_recipient, - withdrawals, - parent_beacon_block_root: attributes.inner.parent_beacon_block_root, + let proposal = ProposalAttributes { + builder_fee_recipient, + suggested_gas_limit: proposer.gas_limit, + proposer_fee_recipient: proposer.fee_recipient, }; - - BuilderPayloadBuilderAttributes::try_new(attributes.inner.parent, payload_attributes) - .expect("conversion currently always succeeds") + let mut attributes = attributes.clone(); + attributes.attach_proposal(proposal); + attributes } pub enum KeepAlive { @@ -68,6 +49,7 @@ pub enum Message { pub struct Config { pub fee_recipient: Address, pub genesis_time: Option, + pub extra_data: Option, } pub struct Builder< @@ -80,7 +62,6 @@ pub struct Builder< auctioneer: Sender, payload_builder: PayloadBuilderHandle, payload_store: PayloadStore, - executor: TaskExecutor, config: Config, context: Arc, genesis_time: u64, @@ -97,22 +78,12 @@ impl< msgs: Receiver, auctioneer: Sender, payload_builder: PayloadBuilderHandle, - executor: TaskExecutor, config: Config, context: Arc, genesis_time: u64, ) -> Self { let payload_store = payload_builder.clone().into(); - Self { - msgs, - auctioneer, - payload_builder, - payload_store, - executor, - config, - context, - genesis_time, - } + Self { msgs, auctioneer, payload_builder, payload_store, config, context, genesis_time } } pub async fn process_proposals( @@ -126,11 +97,10 @@ impl< if let Some(proposals) = proposals { for (proposer, relays) in proposals { let attributes = - make_attributes_for_proposer(&attributes, self.config.fee_recipient); + make_attributes_for_proposer(&attributes, self.config.fee_recipient, &proposer); - if let Some(attributes) = self.start_build(&proposer, &attributes).await { - // TODO: can likely skip full attributes in `AuctionContext`, can skip clone in - // `start_build` + if let Some(_) = self.start_build(&attributes).await { + // TODO: can likely skip full attributes in `AuctionContext` let auction = AuctionContext { slot, attributes, proposer, relays }; new_auctions.push(auction); } @@ -140,40 +110,24 @@ impl< } // TODO: can likely skip returning attributes here... - async fn start_build( - &self, - _proposer: &Proposer, - attributes: &BuilderPayloadBuilderAttributes, - ) -> Option { + async fn start_build(&self, attributes: &BuilderPayloadBuilderAttributes) -> Option { match self.payload_builder.new_payload(attributes.clone()).await { Ok(payload_id) => { let attributes_payload_id = attributes.payload_id(); - if payload_id == attributes_payload_id { - Some(attributes.clone()) - } else { - warn!(%payload_id, %attributes_payload_id, "mismatch between computed payload id and the one returned by the payload builder"); - None + if payload_id != attributes_payload_id { + error!(%payload_id, %attributes_payload_id, "mismatch between computed payload id and the one returned by the payload builder"); } + Some(payload_id) } Err(err) => { - warn!(%err, "bulder could not start build with payload builder"); + warn!(%err, "builder could not start build with payload builder"); None } } } - fn terminate_job(&self, payload_id: PayloadId) { - let payload_store = self.payload_store.clone(); - self.executor.spawn(async move { - // NOTE: terminate job and discard any built payload - let _ = payload_store.resolve(payload_id).await; - }); - } - async fn on_payload_attributes(&self, attributes: BuilderPayloadBuilderAttributes) { - // NOTE: the payload builder currently makes a job for the incoming `attributes`. - // We want to customize the building logic and so we cancel this first job unconditionally. - self.terminate_job(attributes.payload_id()); + // TODO: ignore already processed attributes // TODO: move slot calc to auctioneer? let slot = convert_timestamp_to_slot( diff --git a/mev-build-rs/src/payload/builder_attributes.rs b/mev-build-rs/src/payload/builder_attributes.rs index fd966b50..cdc47c7b 100644 --- a/mev-build-rs/src/payload/builder_attributes.rs +++ b/mev-build-rs/src/payload/builder_attributes.rs @@ -10,10 +10,14 @@ use reth::{ compat::engine::convert_standalone_withdraw_to_withdrawal, types::engine::PayloadAttributes, }, }; +use sha2::Digest; use std::convert::Infallible; -pub fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId { - use sha2::Digest; +pub fn payload_id_with_bytes( + parent: &B256, + attributes: &PayloadAttributes, + proposal: Option<&ProposalAttributes>, +) -> (PayloadId, [u8; 8]) { let mut hasher = sha2::Sha256::new(); hasher.update(parent.as_slice()); hasher.update(&attributes.timestamp.to_be_bytes()[..]); @@ -29,18 +33,50 @@ pub fn payload_id(parent: &B256, attributes: &PayloadAttributes) -> PayloadId { hasher.update(parent_beacon_block); } + if let Some(proposal) = proposal { + hasher.update(proposal.suggested_gas_limit.to_be_bytes()); + hasher.update(proposal.proposer_fee_recipient.as_slice()); + } + + let out = hasher.finalize(); + let inner: [u8; 8] = out.as_slice()[..8].try_into().expect("sufficient length"); + (PayloadId::new(inner.clone()), inner) +} + +pub fn mix_proposal_into_payload_id( + payload_id: [u8; 8], + proposal: &ProposalAttributes, +) -> PayloadId { + let mut hasher = sha2::Sha256::new(); + hasher.update(payload_id); + + hasher.update(proposal.builder_fee_recipient.as_slice()); + hasher.update(proposal.suggested_gas_limit.to_be_bytes()); + hasher.update(proposal.proposer_fee_recipient.as_slice()); + let out = hasher.finalize(); PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length")) } -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] +pub struct ProposalAttributes { + pub builder_fee_recipient: Address, + pub suggested_gas_limit: u64, + pub proposer_fee_recipient: Address, +} + +#[derive(Debug, Clone)] pub struct BuilderPayloadBuilderAttributes { pub inner: EthPayloadBuilderAttributes, + // TODO: can skip this if we expose the inner value upstream + // NOTE: save this here to avoid recomputing later + payload_id: Option<[u8; 8]>, + pub proposal: Option, } impl BuilderPayloadBuilderAttributes { pub fn new(parent: B256, attributes: PayloadAttributes) -> Self { - let id = payload_id(&parent, &attributes); + let (id, id_bytes) = payload_id_with_bytes(&parent, &attributes, None); let withdraw = attributes.withdrawals.map(|withdrawals| { Withdrawals::new( @@ -60,7 +96,16 @@ impl BuilderPayloadBuilderAttributes { withdrawals: withdraw.unwrap_or_default(), parent_beacon_block_root: attributes.parent_beacon_block_root, }; - Self { inner } + Self { inner, payload_id: Some(id_bytes), proposal: None } + } + + pub fn attach_proposal(&mut self, proposal: ProposalAttributes) { + // NOTE: error to call this more than once; see note on this field, hopefully this goes away + if let Some(payload_id) = self.payload_id.take() { + let id = mix_proposal_into_payload_id(payload_id, &proposal); + self.inner.id = id; + self.proposal = Some(proposal); + } } } diff --git a/mev-build-rs/src/payload/job.rs b/mev-build-rs/src/payload/job.rs index f8678352..11682ad9 100644 --- a/mev-build-rs/src/payload/job.rs +++ b/mev-build-rs/src/payload/job.rs @@ -1,4 +1,7 @@ -use crate::utils::payload_job::{PayloadTaskGuard, PendingPayload, ResolveBestPayload}; +use crate::{ + payload::builder_attributes::BuilderPayloadBuilderAttributes, + utils::payload_job::{PayloadTaskGuard, PendingPayload, ResolveBestPayload}, +}; use futures_util::{Future, FutureExt}; use reth::{ api::BuiltPayload, @@ -42,11 +45,13 @@ where Client: StateProviderFactory + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, - Builder: PayloadBuilder + Unpin + 'static, + Builder: PayloadBuilder + + Unpin + + 'static, >::Attributes: Unpin + Clone, >::BuiltPayload: Unpin + Clone, { - type PayloadAttributes = Builder::Attributes; + type PayloadAttributes = BuilderPayloadBuilderAttributes; type ResolvePayloadFuture = ResolveBestPayload; type BuiltPayload = Builder::BuiltPayload; @@ -60,6 +65,7 @@ where // Note: it is assumed that this is unlikely to happen, as the payload job is started right // away and the first full block should have been built by the time CL is requesting the // payload. + // TODO: customize with proposer payment Builder::build_empty_payload(&self.client, self.config.clone()) } @@ -177,7 +183,6 @@ where BuildOutcome::Better { payload, cached_reads } => { this.cached_reads = Some(cached_reads); debug!(target: "payload_builder", value = %payload.fees(), "built better payload"); - let payload = payload; this.best_payload = Some(payload); } BuildOutcome::Aborted { fees, cached_reads } => { diff --git a/mev-build-rs/src/payload/job_generator.rs b/mev-build-rs/src/payload/job_generator.rs index a36d0efa..09b73754 100644 --- a/mev-build-rs/src/payload/job_generator.rs +++ b/mev-build-rs/src/payload/job_generator.rs @@ -1,5 +1,5 @@ use crate::{ - payload::job::PayloadJob, + payload::{builder_attributes::BuilderPayloadBuilderAttributes, job::PayloadJob}, utils::payload_job::{duration_until, PayloadTaskGuard}, }; use reth::{ @@ -82,7 +82,9 @@ where Client: StateProviderFactory + BlockReaderIdExt + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + Unpin + 'static, - Builder: PayloadBuilder + Unpin + 'static, + Builder: PayloadBuilder + + Unpin + + 'static, >::Attributes: Unpin + Clone, >::BuiltPayload: Unpin + Clone, { @@ -90,7 +92,7 @@ where fn new_payload_job( &self, - attributes: >::Attributes, + attributes: ::PayloadAttributes, ) -> Result { let parent_block = if attributes.parent().is_zero() { // use latest block if parent is zero: genesis block @@ -108,6 +110,14 @@ where block.seal(attributes.parent()) }; + let until = if attributes.proposal.is_some() { + self.job_deadline(attributes.timestamp()) + } else { + // If there is no attached proposal, then terminate the payload job immediately + tokio::time::Instant::now() + }; + let deadline = Box::pin(tokio::time::sleep_until(until)); + let config = PayloadConfig::new( Arc::new(parent_block), self.config.extradata.clone(), @@ -115,9 +125,6 @@ where Arc::clone(&self.chain_spec), ); - let until = self.job_deadline(config.attributes.timestamp()); - let deadline = Box::pin(tokio::time::sleep_until(until)); - let cached_reads = self.maybe_pre_cached(config.parent_block.hash()); Ok(PayloadJob { diff --git a/mev-build-rs/src/payload/service_builder.rs b/mev-build-rs/src/payload/service_builder.rs index 9ff291ce..ae309290 100644 --- a/mev-build-rs/src/payload/service_builder.rs +++ b/mev-build-rs/src/payload/service_builder.rs @@ -9,12 +9,15 @@ use reth::{ builder::{node::FullNodeTypes, BuilderContext}, cli::config::PayloadBuilderConfig, payload::{PayloadBuilderHandle, PayloadBuilderService}, + primitives::Bytes, providers::CanonStateSubscriptions, transaction_pool::TransactionPool, }; -#[derive(Debug, Clone, Copy, Default)] -pub struct PayloadServiceBuilder; +#[derive(Debug, Clone, Default)] +pub struct PayloadServiceBuilder { + pub extra_data: Option, +} impl reth::builder::components::PayloadServiceBuilder for PayloadServiceBuilder @@ -29,8 +32,13 @@ where ) -> eyre::Result> { let conf = ctx.payload_builder_config(); + let extradata = if let Some(extra_data) = self.extra_data { + extra_data + } else { + conf.extradata_bytes() + }; let payload_job_config = PayloadJobGeneratorConfig { - extradata: conf.extradata_bytes(), + extradata, _max_gas_limit: conf.max_gas_limit(), interval: conf.interval(), deadline: conf.deadline(), diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index 4eb90948..8885e983 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -2,7 +2,9 @@ use crate::{ auctioneer::{Auctioneer, Config as AuctioneerConfig}, builder::{Builder, Config as BuilderConfig}, node::BuilderNode, - payload::builder_attributes::BuilderPayloadBuilderAttributes, + payload::{ + builder_attributes::BuilderPayloadBuilderAttributes, service_builder::PayloadServiceBuilder, + }, }; use ethereum_consensus::{ clock::SystemClock, networks::Network, primitives::Epoch, state_transition::Context, @@ -12,7 +14,6 @@ use reth::{ api::EngineTypes, builder::{InitState, WithLaunchContext}, payload::{EthBuiltPayload, PayloadBuilderHandle}, - primitives::Bytes, tasks::TaskExecutor, }; use reth_db::DatabaseEnv; @@ -32,8 +33,6 @@ pub const DEFAULT_COMPONENT_CHANNEL_SIZE: usize = 16; #[derive(Deserialize, Debug, Default, Clone)] pub struct Config { - // TODO: move to payload builder - pub extra_data: Bytes, // TODO: move to payload builder pub execution_mnemonic: String, // TODO: move to bidder @@ -91,7 +90,6 @@ pub async fn construct< builder_rx, auctioneer_tx, payload_builder, - task_executor.clone(), config.builder, context.clone(), genesis_time, @@ -122,9 +120,11 @@ pub async fn launch( // TODO: ability to just run reth + let payload_builder = PayloadServiceBuilder { extra_data: config.builder.extra_data.clone() }; + let handle = node_builder .with_types(BuilderNode::default()) - .with_components(BuilderNode::components()) + .with_components(BuilderNode::components().payload(payload_builder)) .launch() .await?; diff --git a/mev-build-rs/src/utils/compat.rs b/mev-build-rs/src/utils/compat.rs index f3dfeb5f..b9fe588a 100644 --- a/mev-build-rs/src/utils/compat.rs +++ b/mev-build-rs/src/utils/compat.rs @@ -13,7 +13,7 @@ pub fn to_bytes32(value: B256) -> Bytes32 { Bytes32::try_from(value.as_ref()).unwrap() } -fn to_bytes20(value: Address) -> ExecutionAddress { +pub fn to_bytes20(value: Address) -> ExecutionAddress { ExecutionAddress::try_from(value.as_ref()).unwrap() } From 2d1cbd3bd1b566f13848368d552b9819477e191b Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Wed, 24 Apr 2024 17:14:10 -0600 Subject: [PATCH 03/14] initial progress on finalizing payloads for auction --- Cargo.lock | 70 +++++- Cargo.toml | 2 + example.config.toml | 4 +- mev-build-rs/Cargo.toml | 2 + mev-build-rs/src/auctioneer.rs | 1 + mev-build-rs/src/builder.rs | 54 ++++- mev-build-rs/src/error.rs | 5 +- .../src/payload/builder_attributes.rs | 4 +- mev-build-rs/src/payload/job.rs | 40 +++- mev-build-rs/src/payload/job_generator.rs | 12 +- mev-build-rs/src/payload/mod.rs | 1 + mev-build-rs/src/payload/resolve.rs | 225 ++++++++++++++++++ mev-build-rs/src/service.rs | 4 +- 13 files changed, 392 insertions(+), 32 deletions(-) create mode 100644 mev-build-rs/src/payload/resolve.rs diff --git a/Cargo.lock b/Cargo.lock index 5e6dcec2..b2939714 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,34 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloy-json-rpc" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=188c4f8#188c4f8f6080d4beaaea653c57261cb3b53a95b3" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=188c4f8#188c4f8f6080d4beaaea653c57261cb3b53a95b3" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-primitives", + "alloy-rpc-types", + "alloy-signer", + "async-trait", + "futures-utils-wasm", + "thiserror", +] + [[package]] name = "alloy-primitives" version = "0.7.0" @@ -329,6 +357,36 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloy-signer" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=188c4f8#188c4f8f6080d4beaaea653c57261cb3b53a95b3" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "elliptic-curve 0.13.8", + "k256 0.13.3", + "thiserror", +] + +[[package]] +name = "alloy-signer-wallet" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy?rev=188c4f8#188c4f8f6080d4beaaea653c57261cb3b53a95b3" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "coins-bip32", + "coins-bip39", + "k256 0.13.3", + "rand 0.8.5", + "thiserror", +] + [[package]] name = "alloy-sol-macro" version = "0.7.0" @@ -931,7 +989,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "lazy_static", "lazycell", "proc-macro2", @@ -3097,6 +3155,12 @@ dependencies = [ "slab", ] +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + [[package]] name = "fxhash" version = "0.2.1" @@ -4251,7 +4315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -4701,6 +4765,8 @@ dependencies = [ name = "mev-build-rs" version = "0.3.0" dependencies = [ + "alloy-signer", + "alloy-signer-wallet", "async-trait", "beacon-api-client", "clap", diff --git a/Cargo.toml b/Cargo.toml index 82439b11..7b00904e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ reth-db = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "71f8e678aa53f75c2c35badaa5848262de594cdc" } +alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "188c4f8" } +alloy-signer-wallet = { git = "https://github.com/alloy-rs/alloy", rev = "188c4f8" } eyre = "0.6.8" futures-util = "0.3.30" diff --git a/example.config.toml b/example.config.toml index f2c89b53..c5021881 100644 --- a/example.config.toml +++ b/example.config.toml @@ -19,8 +19,6 @@ accepted_builders = [ ] [builder] -# wallet seed for builder to author payment transactions -execution_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" # number of milliseconds to submit bids ahead of the target slot bidding_deadline_ms = 1000 # amount of value to bid as a fraction of the payload's revenue @@ -41,3 +39,5 @@ relays = [ fee_recipient = "0x0000000000000000000000000000000000000000" # [optional] extra data to write into built execution payload extra_data = "0x68656C6C6F20776F726C640A" # "hello world" +# wallet seed for builder to author payment transactions +execution_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" diff --git a/mev-build-rs/Cargo.toml b/mev-build-rs/Cargo.toml index caae21dc..688cdf10 100644 --- a/mev-build-rs/Cargo.toml +++ b/mev-build-rs/Cargo.toml @@ -27,6 +27,8 @@ reth-db = { workspace = true } reth-node-ethereum = { workspace = true } reth-evm-ethereum = { workspace = true } reth-basic-payload-builder = { workspace = true } +alloy-signer = { workspace = true } +alloy-signer-wallet = { workspace = true, features = ["mnemonic"] } sha2 = { workspace = true } ethers = "2.0" diff --git a/mev-build-rs/src/auctioneer.rs b/mev-build-rs/src/auctioneer.rs index b6f9f96c..84a3fb0a 100644 --- a/mev-build-rs/src/auctioneer.rs +++ b/mev-build-rs/src/auctioneer.rs @@ -165,6 +165,7 @@ impl Auctioneer { async fn submit_payload(&self, payload: EthBuiltPayload) { let auction = self.open_auctions.get(&payload.id()).expect("has auction"); + // TODO: should convert to ExecutionPayloadV3 etc. for blobs etc. match prepare_submission( payload, &self.config.secret_key, diff --git a/mev-build-rs/src/builder.rs b/mev-build-rs/src/builder.rs index 739412a3..16c95237 100644 --- a/mev-build-rs/src/builder.rs +++ b/mev-build-rs/src/builder.rs @@ -5,6 +5,7 @@ use crate::{ payload::builder_attributes::{BuilderPayloadBuilderAttributes, ProposalAttributes}, Error, }; +use alloy_signer_wallet::{coins_bip39::English, LocalWallet, MnemonicBuilder}; use ethereum_consensus::{ clock::convert_timestamp_to_slot, primitives::Slot, state_transition::Context, }; @@ -25,10 +26,12 @@ use tracing::{error, warn}; fn make_attributes_for_proposer( attributes: &BuilderPayloadBuilderAttributes, builder_fee_recipient: Address, + builder_signer: Arc, proposer: &Proposer, ) -> BuilderPayloadBuilderAttributes { let proposal = ProposalAttributes { builder_fee_recipient, + builder_signer, suggested_gas_limit: proposer.gas_limit, proposer_fee_recipient: proposer.fee_recipient, }; @@ -50,6 +53,7 @@ pub struct Config { pub fee_recipient: Address, pub genesis_time: Option, pub extra_data: Option, + pub execution_mnemonic: String, } pub struct Builder< @@ -65,6 +69,7 @@ pub struct Builder< config: Config, context: Arc, genesis_time: u64, + signer: Arc, } impl< @@ -83,7 +88,20 @@ impl< genesis_time: u64, ) -> Self { let payload_store = payload_builder.clone().into(); - Self { msgs, auctioneer, payload_builder, payload_store, config, context, genesis_time } + let signer = MnemonicBuilder::::default() + .phrase(&config.execution_mnemonic) + .build() + .expect("is valid"); + Self { + msgs, + auctioneer, + payload_builder, + payload_store, + config, + context, + genesis_time, + signer: Arc::new(signer), + } } pub async fn process_proposals( @@ -96,8 +114,12 @@ impl< if let Some(proposals) = proposals { for (proposer, relays) in proposals { - let attributes = - make_attributes_for_proposer(&attributes, self.config.fee_recipient, &proposer); + let attributes = make_attributes_for_proposer( + &attributes, + self.config.fee_recipient, + self.signer.clone(), + &proposer, + ); if let Some(_) = self.start_build(&attributes).await { // TODO: can likely skip full attributes in `AuctionContext` @@ -154,17 +176,23 @@ impl< } async fn send_payload_to_auctioneer(&self, payload_id: PayloadId, _keep_alive: KeepAlive) { - // TODO: use signal from bidder to know if we should keep refining a given payload, or can - // extract the final build - match self.payload_store.best_payload(payload_id).await.expect("exists") { - Ok(payload) => self - .auctioneer - .send(AuctioneerMessage::BuiltPayload(payload)) - .await - .expect("can send"), - Err(err) => { - warn!(%err, "could not get payload when requested") + // TODO: put into separate task? + // TODO: signal to payload job `_keep_alive` status + let maybe_payload = self.payload_store.resolve(payload_id).await; + if let Some(payload) = maybe_payload { + match payload { + // TODO: auctioneer can just listen for payload events instead + Ok(payload) => self + .auctioneer + .send(AuctioneerMessage::BuiltPayload(payload)) + .await + .expect("can send"), + Err(err) => { + warn!(%err, %payload_id, "error resolving payload") + } } + } else { + warn!(%payload_id, "could not resolve payload") } } diff --git a/mev-build-rs/src/error.rs b/mev-build-rs/src/error.rs index 0ab36165..64c045d3 100644 --- a/mev-build-rs/src/error.rs +++ b/mev-build-rs/src/error.rs @@ -1,8 +1,11 @@ use ethereum_consensus::Error as ConsensusError; +use reth::payload::error::PayloadBuilderError; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { - #[error("{0}")] + #[error(transparent)] Consensus(#[from] ConsensusError), + #[error(transparent)] + PayloadBuilderError(#[from] PayloadBuilderError), } diff --git a/mev-build-rs/src/payload/builder_attributes.rs b/mev-build-rs/src/payload/builder_attributes.rs index cdc47c7b..4b84c04b 100644 --- a/mev-build-rs/src/payload/builder_attributes.rs +++ b/mev-build-rs/src/payload/builder_attributes.rs @@ -1,3 +1,4 @@ +use alloy_signer_wallet::LocalWallet; use reth::{ api::PayloadBuilderAttributes, payload::{EthPayloadBuilderAttributes, PayloadId}, @@ -11,7 +12,7 @@ use reth::{ }, }; use sha2::Digest; -use std::convert::Infallible; +use std::{convert::Infallible, sync::Arc}; pub fn payload_id_with_bytes( parent: &B256, @@ -61,6 +62,7 @@ pub fn mix_proposal_into_payload_id( #[derive(Debug, Clone)] pub struct ProposalAttributes { pub builder_fee_recipient: Address, + pub builder_signer: Arc, pub suggested_gas_limit: u64, pub proposer_fee_recipient: Address, } diff --git a/mev-build-rs/src/payload/job.rs b/mev-build-rs/src/payload/job.rs index 11682ad9..936f3ba1 100644 --- a/mev-build-rs/src/payload/job.rs +++ b/mev-build-rs/src/payload/job.rs @@ -1,11 +1,17 @@ use crate::{ - payload::builder_attributes::BuilderPayloadBuilderAttributes, + payload::{ + builder_attributes::BuilderPayloadBuilderAttributes, + resolve::{PayloadFinalizer, PayloadFinalizerConfig, ResolveBuilderPayload}, + }, utils::payload_job::{PayloadTaskGuard, PendingPayload, ResolveBestPayload}, }; use futures_util::{Future, FutureExt}; use reth::{ api::BuiltPayload, - payload::{self, database::CachedReads, error::PayloadBuilderError, KeepPayloadJobAlive}, + payload::{ + self, database::CachedReads, error::PayloadBuilderError, EthBuiltPayload, + KeepPayloadJobAlive, + }, providers::StateProviderFactory, tasks::TaskSpawner, transaction_pool::TransactionPool, @@ -45,16 +51,21 @@ where Client: StateProviderFactory + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, - Builder: PayloadBuilder - + Unpin + Builder: PayloadBuilder< + Pool, + Client, + Attributes = BuilderPayloadBuilderAttributes, + BuiltPayload = EthBuiltPayload, + > + Unpin + 'static, >::Attributes: Unpin + Clone, >::BuiltPayload: Unpin + Clone, { type PayloadAttributes = BuilderPayloadBuilderAttributes; - type ResolvePayloadFuture = ResolveBestPayload; + type ResolvePayloadFuture = ResolveBuilderPayload; type BuiltPayload = Builder::BuiltPayload; + // TODO: do we need to customize this? if not, use default impl in some way fn best_payload(&self) -> Result { if let Some(ref payload) = self.best_payload { return Ok(payload.clone()) @@ -116,7 +127,24 @@ where let fut = ResolveBestPayload { best_payload, maybe_better, empty_payload }; - (fut, KeepPayloadJobAlive::No) + let config = + self.config.attributes.proposal.as_ref().map(|attributes| PayloadFinalizerConfig { + proposer_fee_recipient: attributes.proposer_fee_recipient, + signer: attributes.builder_signer.clone(), + sender: Default::default(), + parent_hash: self.config.parent_block.hash(), + chain_id: self.config.chain_spec.chain().id(), + cfg_env: self.config.initialized_cfg.clone(), + block_env: self.config.initialized_block_env.clone(), + }); + let finalizer = PayloadFinalizer { + client: self.client.clone(), + _pool: self.pool.clone(), + payload_id: self.config.payload_id(), + config, + }; + + (ResolveBuilderPayload { resolution: fut, finalizer }, KeepPayloadJobAlive::No) } } diff --git a/mev-build-rs/src/payload/job_generator.rs b/mev-build-rs/src/payload/job_generator.rs index 09b73754..fac4b5fa 100644 --- a/mev-build-rs/src/payload/job_generator.rs +++ b/mev-build-rs/src/payload/job_generator.rs @@ -4,7 +4,7 @@ use crate::{ }; use reth::{ api::PayloadBuilderAttributes, - payload::{self, database::CachedReads, error::PayloadBuilderError}, + payload::{self, database::CachedReads, error::PayloadBuilderError, EthBuiltPayload}, primitives::{BlockNumberOrTag, Bytes, ChainSpec, B256}, providers::{BlockReaderIdExt, BlockSource, CanonStateNotification, StateProviderFactory}, tasks::TaskSpawner, @@ -16,13 +16,13 @@ use std::{sync::Arc, time::Duration}; #[derive(Debug, Clone)] pub struct PayloadJobGeneratorConfig { pub extradata: Bytes, + // TODO: remove or use? pub _max_gas_limit: u64, pub interval: Duration, pub deadline: Duration, pub max_payload_tasks: usize, } -/// The generator type that creates new jobs that builds empty blocks. #[derive(Debug)] pub struct PayloadJobGenerator { client: Client, @@ -82,8 +82,12 @@ where Client: StateProviderFactory + BlockReaderIdExt + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + Unpin + 'static, - Builder: PayloadBuilder - + Unpin + Builder: PayloadBuilder< + Pool, + Client, + Attributes = BuilderPayloadBuilderAttributes, + BuiltPayload = EthBuiltPayload, + > + Unpin + 'static, >::Attributes: Unpin + Clone, >::BuiltPayload: Unpin + Clone, diff --git a/mev-build-rs/src/payload/mod.rs b/mev-build-rs/src/payload/mod.rs index 4a97eb3c..de7cbe2c 100644 --- a/mev-build-rs/src/payload/mod.rs +++ b/mev-build-rs/src/payload/mod.rs @@ -4,4 +4,5 @@ pub mod builder; pub mod builder_attributes; pub mod job; pub mod job_generator; +pub mod resolve; pub mod service_builder; diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs new file mode 100644 index 00000000..d09e083d --- /dev/null +++ b/mev-build-rs/src/payload/resolve.rs @@ -0,0 +1,225 @@ +//! Resolve a given payload for use in the auction +//! Takes a payload from the payload builder and "finalizes" the crafted payload to yield a valid +//! block according to the auction rules. + +use crate::utils::payload_job::ResolveBestPayload; +use alloy_signer::SignerSync; +use alloy_signer_wallet::LocalWallet; +use futures_util::FutureExt; +use reth::{ + api::BuiltPayload, + payload::{error::PayloadBuilderError, EthBuiltPayload, PayloadId}, + primitives::{ + proofs, revm::env::tx_env_with_recovered, Address, Block, ChainId, Receipt, Receipts, + SealedBlock, Signature, Transaction, TransactionKind, TransactionSigned, + TransactionSignedEcRecovered, TxEip1559, B256, U256, + }, + providers::{BundleStateWithReceipts, StateProviderFactory}, + revm::{ + self, + database::StateProviderDatabase, + db::states::bundle_state::BundleRetention, + primitives::{BlockEnv, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, ResultAndState}, + DatabaseCommit, State, + }, +}; +use std::{ + future::Future, + pin::Pin, + sync::Arc, + task::{ready, Context, Poll}, +}; + +pub const BASE_TX_GAS_LIMIT: u64 = 21000; + +fn make_payment_transaction( + config: &PayloadFinalizerConfig, + nonce: u64, + max_fee_per_gas: u128, + value: U256, +) -> Result { + let tx = Transaction::Eip1559(TxEip1559 { + chain_id: config.chain_id, + nonce, + gas_limit: BASE_TX_GAS_LIMIT, + // SAFETY: cast to bigger type always succeeds + max_fee_per_gas, + max_priority_fee_per_gas: 0, + to: TransactionKind::Call(config.sender), + value, + access_list: Default::default(), + input: Default::default(), + }); + // TODO: verify we are signing correctly... + let signature_hash = tx.signature_hash(); + let signature = config.signer.sign_hash_sync(&signature_hash).expect("can sign"); + let signed_transaction = TransactionSigned::from_transaction_and_signature( + tx, + Signature { r: signature.r(), s: signature.s(), odd_y_parity: signature.v().y_parity() }, + ); + Ok(TransactionSignedEcRecovered::from_signed_transaction(signed_transaction, config.sender)) +} + +fn append_payment( + client: &Client, + config: &PayloadFinalizerConfig, + block: SealedBlock, + value: U256, +) -> Result { + let state_provider = client.state_by_block_hash(config.parent_hash)?; + let state = StateProviderDatabase::new(&state_provider); + // TODO: use cached reads + let mut db = State::builder().with_database_ref(state).with_bundle_update().build(); + + let signer_account = db.load_cache_account(config.sender)?; + // TODO handle option + let nonce = signer_account.account_info().expect("account exists").nonce; + // TODO handle option + let max_fee_per_gas = block.header().base_fee_per_gas.expect("exists") as u128; + let payment_tx = make_payment_transaction(config, nonce, max_fee_per_gas, value)?; + + // === Apply txn === + + // TODO: try to clone the envs less here... + let env = EnvWithHandlerCfg::new_with_cfg_env( + config.cfg_env.clone(), + config.block_env.clone(), + tx_env_with_recovered(&payment_tx), + ); + let mut evm = revm::Evm::builder().with_db(&mut db).with_env_with_handler_cfg(env).build(); + + let ResultAndState { result, state } = + evm.transact().map_err(PayloadBuilderError::EvmExecutionError)?; + + drop(evm); + db.commit(state); + + let Block { mut header, mut body, ommers, withdrawals } = block.unseal(); + + // TODO: hold gas reserve so this always succeeds + let cumulative_gas_used = header.gas_used + result.gas_used(); + // TODO: sanity check we didn't go over gas limit + let receipt = Receipt { + tx_type: payment_tx.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.into_logs().into_iter().map(Into::into).collect(), + ..Default::default() + }; + + body.push(payment_tx.into_signed()); + + db.merge_transitions(BundleRetention::PlainState); + + let block_number = header.number; + // TODO: this is broken bc we need to keep receipts... + // NOTE: need to either fetch receipts from DB, or just keep state and not build in two steps + // here... + // May want to pass bundle state from payload builder and then `extend` here instead... + let bundle = BundleStateWithReceipts::new( + db.take_bundle(), + Receipts::from_vec(vec![vec![Some(receipt)]]), + block_number, + ); + + let receipts_root = bundle.receipts_root_slow(block_number).expect("Number is in range"); + let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); + + // calculate the state root + let state_root = state_provider.state_root(bundle.state())?; + + // create the block header + let transactions_root = proofs::calculate_transaction_root(&body); + + header.state_root = state_root; + header.transactions_root = transactions_root; + header.receipts_root = receipts_root; + header.logs_bloom = logs_bloom; + header.gas_used = cumulative_gas_used; + + let block = Block { header, body, ommers, withdrawals }; + + Ok(block.seal_slow()) +} + +#[derive(Debug)] +pub struct PayloadFinalizerConfig { + pub proposer_fee_recipient: Address, + pub signer: Arc, + pub sender: Address, + pub parent_hash: B256, + pub chain_id: ChainId, + pub cfg_env: CfgEnvWithHandlerCfg, + pub block_env: BlockEnv, +} + +#[derive(Debug)] +pub struct PayloadFinalizer { + pub client: Client, + pub _pool: Pool, + pub payload_id: PayloadId, + pub config: Option, +} + +impl PayloadFinalizer { + fn determine_payment_amount(&self, fees: U256) -> U256 { + // TODO: get amount to bid from bidder + // - amount from block fees + // - including any subsidy + fees + } + + fn prepare( + &self, + block: &SealedBlock, + fees: U256, + config: &PayloadFinalizerConfig, + ) -> Result { + let payment_amount = self.determine_payment_amount(fees); + let block = append_payment(&self.client, config, block.clone(), payment_amount)?; + // TODO: - track proposer payment, revenue + Ok(EthBuiltPayload::new(self.payload_id, block, fees)) + } + + fn process( + &mut self, + block: &SealedBlock, + fees: U256, + ) -> Result { + if let Some(config) = self.config.as_ref() { + self.prepare(block, fees, config) + } else { + Ok(EthBuiltPayload::new(self.payload_id, block.clone(), fees)) + } + } +} + +#[derive(Debug)] +pub struct ResolveBuilderPayload { + pub resolution: ResolveBestPayload, + pub finalizer: PayloadFinalizer, +} + +impl Future for ResolveBuilderPayload +where + Payload: BuiltPayload + Unpin, + Client: StateProviderFactory + Unpin, + Pool: Unpin, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let payload = ready!(this.resolution.poll_unpin(cx))?; + + // TODO: save payload in the event we need to poll again? + + // TODO: we are dropping blobs.... + + let block = payload.block(); + let fees = payload.fees(); + + let finalized_payload = this.finalizer.process(block, fees); + Poll::Ready(finalized_payload) + } +} diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index 8885e983..dc671642 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -33,12 +33,10 @@ pub const DEFAULT_COMPONENT_CHANNEL_SIZE: usize = 16; #[derive(Deserialize, Debug, Default, Clone)] pub struct Config { - // TODO: move to payload builder - pub execution_mnemonic: String, // TODO: move to bidder // amount in milliseconds pub bidding_deadline_ms: u64, - // TODO: move to payload builder, or have as part of bidder config + // TODO: move to bidder // amount to bid as a fraction of the block's value pub bid_percent: Option, // TODO: move to bidder From 4c92f06b018aefcedd726d488ca72cc1e065a948 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 25 Apr 2024 12:15:13 -0600 Subject: [PATCH 04/14] route build state for use when finalizing payload --- mev-build-rs/src/builder.rs | 2 +- mev-build-rs/src/payload/builder.rs | 49 ++++++++++++++++--- .../src/payload/builder_attributes.rs | 2 +- mev-build-rs/src/payload/job.rs | 47 +++++++----------- mev-build-rs/src/payload/job_generator.rs | 28 ++++------- mev-build-rs/src/payload/resolve.rs | 48 +++++++++--------- mev-build-rs/src/service.rs | 2 +- 7 files changed, 98 insertions(+), 80 deletions(-) diff --git a/mev-build-rs/src/builder.rs b/mev-build-rs/src/builder.rs index 16c95237..0c9fbe8f 100644 --- a/mev-build-rs/src/builder.rs +++ b/mev-build-rs/src/builder.rs @@ -121,7 +121,7 @@ impl< &proposer, ); - if let Some(_) = self.start_build(&attributes).await { + if self.start_build(&attributes).await.is_some() { // TODO: can likely skip full attributes in `AuctionContext` let auction = AuctionContext { slot, attributes, proposer, relays }; new_auctions.push(auction); diff --git a/mev-build-rs/src/payload/builder.rs b/mev-build-rs/src/payload/builder.rs index d3ec258b..b06f143c 100644 --- a/mev-build-rs/src/payload/builder.rs +++ b/mev-build-rs/src/payload/builder.rs @@ -1,7 +1,7 @@ use crate::payload::builder_attributes::BuilderPayloadBuilderAttributes; use reth::{ api::PayloadBuilderAttributes, - payload::{error::PayloadBuilderError, EthBuiltPayload}, + payload::{error::PayloadBuilderError, EthBuiltPayload, PayloadId}, primitives::{ constants::{ eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, @@ -25,10 +25,35 @@ use reth_basic_payload_builder::{ commit_withdrawals, is_better_payload, pre_block_beacon_root_contract_call, BuildArguments, BuildOutcome, PayloadConfig, WithdrawalsOutcome, }; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, Mutex}, +}; use tracing::{debug, trace, warn}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PayloadBuilder; +#[derive(Debug, Clone, Default)] +pub struct PayloadBuilder(Arc); + +impl Deref for PayloadBuilder { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Default)] +pub struct Inner { + states: Arc>>, +} + +impl Inner { + pub fn get_build_state(&self, payload_id: PayloadId) -> Option { + let mut state = self.states.lock().expect("can lock"); + state.remove(&payload_id) + } +} impl reth_basic_payload_builder::PayloadBuilder for PayloadBuilder where @@ -42,13 +67,21 @@ where &self, args: BuildArguments, ) -> Result, PayloadBuilderError> { - default_ethereum_payload_builder(args) + let payload_id = args.config.payload_id(); + let (outcome, bundle) = default_ethereum_payload_builder(args)?; + if let Some(bundle) = bundle { + let mut states = self.states.lock().expect("can lock"); + states.insert(payload_id, bundle); + } + Ok(outcome) } fn build_empty_payload( client: &Client, config: PayloadConfig, ) -> Result { + // TODO: this should also store bundle state for finalization -- do we need to keep it + // separate from the main driver? let extra_data = config.extra_data(); let PayloadConfig { initialized_block_env, @@ -159,7 +192,7 @@ where #[inline] pub fn default_ethereum_payload_builder( args: BuildArguments, -) -> Result, PayloadBuilderError> +) -> Result<(BuildOutcome, Option), PayloadBuilderError> where Client: StateProviderFactory, Pool: TransactionPool, @@ -220,7 +253,7 @@ where // check if the job was cancelled, if so we can exit early if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) + return Ok((BuildOutcome::Cancelled, None)) } // convert tx to a signed transaction @@ -319,7 +352,7 @@ where // check if we have a better block if !is_better_payload(best_payload.as_ref(), total_fees) { // can skip building the block - return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) + return Ok((BuildOutcome::Aborted { fees: total_fees, cached_reads }, None)) } let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( @@ -406,5 +439,5 @@ where // extend the payload with the blob sidecars from the executed txs payload.extend_sidecars(blob_sidecars); - Ok(BuildOutcome::Better { payload, cached_reads }) + Ok((BuildOutcome::Better { payload, cached_reads }, Some(bundle))) } diff --git a/mev-build-rs/src/payload/builder_attributes.rs b/mev-build-rs/src/payload/builder_attributes.rs index 4b84c04b..e08bbe55 100644 --- a/mev-build-rs/src/payload/builder_attributes.rs +++ b/mev-build-rs/src/payload/builder_attributes.rs @@ -41,7 +41,7 @@ pub fn payload_id_with_bytes( let out = hasher.finalize(); let inner: [u8; 8] = out.as_slice()[..8].try_into().expect("sufficient length"); - (PayloadId::new(inner.clone()), inner) + (PayloadId::new(inner), inner) } pub fn mix_proposal_into_payload_id( diff --git a/mev-build-rs/src/payload/job.rs b/mev-build-rs/src/payload/job.rs index 936f3ba1..69ba171c 100644 --- a/mev-build-rs/src/payload/job.rs +++ b/mev-build-rs/src/payload/job.rs @@ -1,5 +1,6 @@ use crate::{ payload::{ + builder::PayloadBuilder, builder_attributes::BuilderPayloadBuilderAttributes, resolve::{PayloadFinalizer, PayloadFinalizerConfig, ResolveBuilderPayload}, }, @@ -7,7 +8,7 @@ use crate::{ }; use futures_util::{Future, FutureExt}; use reth::{ - api::BuiltPayload, + api::PayloadBuilderAttributes, payload::{ self, database::CachedReads, error::PayloadBuilderError, EthBuiltPayload, KeepPayloadJobAlive, @@ -17,7 +18,7 @@ use reth::{ transaction_pool::TransactionPool, }; use reth_basic_payload_builder::{ - BuildArguments, BuildOutcome, Cancelled, PayloadBuilder, PayloadConfig, + BuildArguments, BuildOutcome, Cancelled, PayloadBuilder as _, PayloadConfig, }; use std::{ pin::Pin, @@ -29,41 +30,29 @@ use tokio::{ }; use tracing::{debug, trace}; -pub struct PayloadJob -where - Builder: PayloadBuilder, -{ - pub config: PayloadConfig, +pub struct PayloadJob { + pub config: PayloadConfig, pub client: Client, pub pool: Pool, pub executor: Tasks, pub deadline: Pin>, pub interval: Interval, - pub best_payload: Option, - pub pending_block: Option>, + pub best_payload: Option, + pub pending_block: Option>, pub payload_task_guard: PayloadTaskGuard, pub cached_reads: Option, - pub builder: Builder, + pub builder: PayloadBuilder, } -impl payload::PayloadJob for PayloadJob +impl payload::PayloadJob for PayloadJob where Client: StateProviderFactory + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, - Builder: PayloadBuilder< - Pool, - Client, - Attributes = BuilderPayloadBuilderAttributes, - BuiltPayload = EthBuiltPayload, - > + Unpin - + 'static, - >::Attributes: Unpin + Clone, - >::BuiltPayload: Unpin + Clone, { type PayloadAttributes = BuilderPayloadBuilderAttributes; type ResolvePayloadFuture = ResolveBuilderPayload; - type BuiltPayload = Builder::BuiltPayload; + type BuiltPayload = EthBuiltPayload; // TODO: do we need to customize this? if not, use default impl in some way fn best_payload(&self) -> Result { @@ -77,7 +66,7 @@ where // away and the first full block should have been built by the time CL is requesting the // payload. // TODO: customize with proposer payment - Builder::build_empty_payload(&self.client, self.config.clone()) + >::build_empty_payload(&self.client, self.config.clone()) } fn payload_attributes(&self) -> Result { @@ -118,7 +107,10 @@ where let client = self.client.clone(); let config = self.config.clone(); self.executor.spawn_blocking(Box::pin(async move { - let res = Builder::build_empty_payload(&client, config); + let res = >::build_empty_payload(&client, config); let _ = tx.send(res); })); @@ -129,13 +121,15 @@ where let config = self.config.attributes.proposal.as_ref().map(|attributes| PayloadFinalizerConfig { + payload_id: self.config.payload_id(), proposer_fee_recipient: attributes.proposer_fee_recipient, signer: attributes.builder_signer.clone(), sender: Default::default(), - parent_hash: self.config.parent_block.hash(), + parent_hash: self.config.attributes.parent(), chain_id: self.config.chain_spec.chain().id(), cfg_env: self.config.initialized_cfg.clone(), block_env: self.config.initialized_block_env.clone(), + builder: self.builder.clone(), }); let finalizer = PayloadFinalizer { client: self.client.clone(), @@ -148,14 +142,11 @@ where } } -impl Future for PayloadJob +impl Future for PayloadJob where Client: StateProviderFactory + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + 'static, - Builder: PayloadBuilder + Unpin + 'static, - >::Attributes: Unpin + Clone, - >::BuiltPayload: Unpin + Clone, { type Output = Result<(), PayloadBuilderError>; diff --git a/mev-build-rs/src/payload/job_generator.rs b/mev-build-rs/src/payload/job_generator.rs index fac4b5fa..c371dc89 100644 --- a/mev-build-rs/src/payload/job_generator.rs +++ b/mev-build-rs/src/payload/job_generator.rs @@ -1,16 +1,16 @@ use crate::{ - payload::{builder_attributes::BuilderPayloadBuilderAttributes, job::PayloadJob}, + payload::{builder::PayloadBuilder, job::PayloadJob}, utils::payload_job::{duration_until, PayloadTaskGuard}, }; use reth::{ api::PayloadBuilderAttributes, - payload::{self, database::CachedReads, error::PayloadBuilderError, EthBuiltPayload}, + payload::{self, database::CachedReads, error::PayloadBuilderError}, primitives::{BlockNumberOrTag, Bytes, ChainSpec, B256}, providers::{BlockReaderIdExt, BlockSource, CanonStateNotification, StateProviderFactory}, tasks::TaskSpawner, transaction_pool::TransactionPool, }; -use reth_basic_payload_builder::{PayloadBuilder, PayloadConfig, PrecachedState}; +use reth_basic_payload_builder::{PayloadConfig, PrecachedState}; use std::{sync::Arc, time::Duration}; #[derive(Debug, Clone)] @@ -24,25 +24,25 @@ pub struct PayloadJobGeneratorConfig { } #[derive(Debug)] -pub struct PayloadJobGenerator { +pub struct PayloadJobGenerator { client: Client, pool: Pool, executor: Tasks, config: PayloadJobGeneratorConfig, payload_task_guard: PayloadTaskGuard, chain_spec: Arc, - builder: Builder, + builder: PayloadBuilder, pre_cached: Option, } -impl PayloadJobGenerator { +impl PayloadJobGenerator { pub fn with_builder( client: Client, pool: Pool, executor: Tasks, config: PayloadJobGeneratorConfig, chain_spec: Arc, - builder: Builder, + builder: PayloadBuilder, ) -> Self { Self { client, @@ -76,23 +76,13 @@ impl PayloadJobGenerator payload::PayloadJobGenerator - for PayloadJobGenerator +impl payload::PayloadJobGenerator for PayloadJobGenerator where Client: StateProviderFactory + BlockReaderIdExt + Clone + Unpin + 'static, Pool: TransactionPool + Unpin + 'static, Tasks: TaskSpawner + Clone + Unpin + 'static, - Builder: PayloadBuilder< - Pool, - Client, - Attributes = BuilderPayloadBuilderAttributes, - BuiltPayload = EthBuiltPayload, - > + Unpin - + 'static, - >::Attributes: Unpin + Clone, - >::BuiltPayload: Unpin + Clone, { - type Job = PayloadJob; + type Job = PayloadJob; fn new_payload_job( &self, diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs index d09e083d..b2cb187a 100644 --- a/mev-build-rs/src/payload/resolve.rs +++ b/mev-build-rs/src/payload/resolve.rs @@ -2,7 +2,7 @@ //! Takes a payload from the payload builder and "finalizes" the crafted payload to yield a valid //! block according to the auction rules. -use crate::utils::payload_job::ResolveBestPayload; +use crate::{payload::builder::PayloadBuilder, utils::payload_job::ResolveBestPayload}; use alloy_signer::SignerSync; use alloy_signer_wallet::LocalWallet; use futures_util::FutureExt; @@ -10,9 +10,9 @@ use reth::{ api::BuiltPayload, payload::{error::PayloadBuilderError, EthBuiltPayload, PayloadId}, primitives::{ - proofs, revm::env::tx_env_with_recovered, Address, Block, ChainId, Receipt, Receipts, - SealedBlock, Signature, Transaction, TransactionKind, TransactionSigned, - TransactionSignedEcRecovered, TxEip1559, B256, U256, + proofs, revm::env::tx_env_with_recovered, Address, Block, ChainId, Receipt, SealedBlock, + Signature, Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, + TxEip1559, B256, U256, }, providers::{BundleStateWithReceipts, StateProviderFactory}, revm::{ @@ -45,7 +45,7 @@ fn make_payment_transaction( // SAFETY: cast to bigger type always succeeds max_fee_per_gas, max_priority_fee_per_gas: 0, - to: TransactionKind::Call(config.sender), + to: TransactionKind::Call(config.proposer_fee_recipient), value, access_list: Default::default(), input: Default::default(), @@ -66,10 +66,21 @@ fn append_payment( block: SealedBlock, value: U256, ) -> Result { + // TODO: can we get some kind of pending state against `block.hash` here instead of replaying + // the bundle state? let state_provider = client.state_by_block_hash(config.parent_hash)?; let state = StateProviderDatabase::new(&state_provider); + let bundle_state_with_receipts = config + .builder + .get_build_state(config.payload_id) + .ok_or_else(|| PayloadBuilderError::Other("missing build state for payload".into()))?; // TODO: use cached reads - let mut db = State::builder().with_database_ref(state).with_bundle_update().build(); + let mut db = State::builder() + .with_database_ref(state) + // TODO skip clone here... + .with_bundle_prestate(bundle_state_with_receipts.state().clone()) + .with_bundle_update() + .build(); let signer_account = db.load_cache_account(config.sender)?; // TODO handle option @@ -80,7 +91,7 @@ fn append_payment( // === Apply txn === - // TODO: try to clone the envs less here... + // TODO: skip clones here let env = EnvWithHandlerCfg::new_with_cfg_env( config.cfg_env.clone(), config.block_env.clone(), @@ -97,38 +108,28 @@ fn append_payment( let Block { mut header, mut body, ommers, withdrawals } = block.unseal(); // TODO: hold gas reserve so this always succeeds - let cumulative_gas_used = header.gas_used + result.gas_used(); // TODO: sanity check we didn't go over gas limit + let cumulative_gas_used = header.gas_used + result.gas_used(); let receipt = Receipt { tx_type: payment_tx.tx_type(), success: result.is_success(), cumulative_gas_used, logs: result.into_logs().into_iter().map(Into::into).collect(), - ..Default::default() }; + // TODO skip clone here + let mut receipts = bundle_state_with_receipts.receipts().clone(); + receipts.push(vec![Some(receipt)]); body.push(payment_tx.into_signed()); db.merge_transitions(BundleRetention::PlainState); let block_number = header.number; - // TODO: this is broken bc we need to keep receipts... - // NOTE: need to either fetch receipts from DB, or just keep state and not build in two steps - // here... - // May want to pass bundle state from payload builder and then `extend` here instead... - let bundle = BundleStateWithReceipts::new( - db.take_bundle(), - Receipts::from_vec(vec![vec![Some(receipt)]]), - block_number, - ); + let bundle = BundleStateWithReceipts::new(db.take_bundle(), receipts, block_number); let receipts_root = bundle.receipts_root_slow(block_number).expect("Number is in range"); let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); - - // calculate the state root let state_root = state_provider.state_root(bundle.state())?; - - // create the block header let transactions_root = proofs::calculate_transaction_root(&body); header.state_root = state_root; @@ -144,6 +145,7 @@ fn append_payment( #[derive(Debug)] pub struct PayloadFinalizerConfig { + pub payload_id: PayloadId, pub proposer_fee_recipient: Address, pub signer: Arc, pub sender: Address, @@ -151,6 +153,7 @@ pub struct PayloadFinalizerConfig { pub chain_id: ChainId, pub cfg_env: CfgEnvWithHandlerCfg, pub block_env: BlockEnv, + pub builder: PayloadBuilder, } #[derive(Debug)] @@ -212,6 +215,7 @@ where let this = self.get_mut(); let payload = ready!(this.resolution.poll_unpin(cx))?; + // TODO: consider making the payment addition `spawn_blocking` // TODO: save payload in the event we need to poll again? // TODO: we are dropping blobs.... diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index dc671642..55de1152 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -121,7 +121,7 @@ pub async fn launch( let payload_builder = PayloadServiceBuilder { extra_data: config.builder.extra_data.clone() }; let handle = node_builder - .with_types(BuilderNode::default()) + .with_types(BuilderNode) .with_components(BuilderNode::components().payload(payload_builder)) .launch() .await?; From 1db7119fac6e0febecc78749a3fcb91a68fa3f6f Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 25 Apr 2024 12:44:35 -0600 Subject: [PATCH 05/14] workaround to support blobs --- mev-build-rs/src/payload/job.rs | 2 +- mev-build-rs/src/payload/resolve.rs | 54 +++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/mev-build-rs/src/payload/job.rs b/mev-build-rs/src/payload/job.rs index 69ba171c..7e635d8e 100644 --- a/mev-build-rs/src/payload/job.rs +++ b/mev-build-rs/src/payload/job.rs @@ -51,7 +51,7 @@ where Tasks: TaskSpawner + Clone + 'static, { type PayloadAttributes = BuilderPayloadBuilderAttributes; - type ResolvePayloadFuture = ResolveBuilderPayload; + type ResolvePayloadFuture = ResolveBuilderPayload; type BuiltPayload = EthBuiltPayload; // TODO: do we need to customize this? if not, use default impl in some way diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs index b2cb187a..215111a5 100644 --- a/mev-build-rs/src/payload/resolve.rs +++ b/mev-build-rs/src/payload/resolve.rs @@ -7,12 +7,14 @@ use alloy_signer::SignerSync; use alloy_signer_wallet::LocalWallet; use futures_util::FutureExt; use reth::{ - api::BuiltPayload, payload::{error::PayloadBuilderError, EthBuiltPayload, PayloadId}, primitives::{ - proofs, revm::env::tx_env_with_recovered, Address, Block, ChainId, Receipt, SealedBlock, - Signature, Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, - TxEip1559, B256, U256, + kzg::{Blob, Bytes48}, + proofs, + revm::env::tx_env_with_recovered, + Address, BlobTransactionSidecar, Block, ChainId, Receipt, SealedBlock, Signature, + Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, + B256, U256, }, providers::{BundleStateWithReceipts, StateProviderFactory}, revm::{ @@ -22,6 +24,7 @@ use reth::{ primitives::{BlockEnv, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, ResultAndState}, DatabaseCommit, State, }, + rpc::types::engine::{BlobsBundleV1, ExecutionPayloadEnvelopeV3}, }; use std::{ future::Future, @@ -174,38 +177,38 @@ impl PayloadFinalizer { fn prepare( &self, - block: &SealedBlock, + block: SealedBlock, fees: U256, config: &PayloadFinalizerConfig, ) -> Result { let payment_amount = self.determine_payment_amount(fees); - let block = append_payment(&self.client, config, block.clone(), payment_amount)?; + let block = append_payment(&self.client, config, block, payment_amount)?; // TODO: - track proposer payment, revenue + // TODO: ensure fees haven't changed Ok(EthBuiltPayload::new(self.payload_id, block, fees)) } fn process( &mut self, - block: &SealedBlock, + block: SealedBlock, fees: U256, ) -> Result { if let Some(config) = self.config.as_ref() { self.prepare(block, fees, config) } else { - Ok(EthBuiltPayload::new(self.payload_id, block.clone(), fees)) + Ok(EthBuiltPayload::new(self.payload_id, block, fees)) } } } #[derive(Debug)] -pub struct ResolveBuilderPayload { - pub resolution: ResolveBestPayload, +pub struct ResolveBuilderPayload { + pub resolution: ResolveBestPayload, pub finalizer: PayloadFinalizer, } -impl Future for ResolveBuilderPayload +impl Future for ResolveBuilderPayload where - Payload: BuiltPayload + Unpin, Client: StateProviderFactory + Unpin, Pool: Unpin, { @@ -220,10 +223,33 @@ where // TODO: we are dropping blobs.... - let block = payload.block(); + let block = payload.block().clone(); let fees = payload.fees(); - let finalized_payload = this.finalizer.process(block, fees); + // TODO: move to custom type to skip copy on blobs + // NOTE: workaround, can move to our own type to skip all this copying + let execution_payload = ExecutionPayloadEnvelopeV3::from(payload); + + let BlobsBundleV1 { commitments, proofs, blobs } = execution_payload.blobs_bundle; + let blob_sidecars = BlobTransactionSidecar { + blobs: blobs + .into_iter() + .map(|blob| Blob::from_bytes(blob.as_ref()).expect("is right size")) + .collect(), + commitments: commitments + .into_iter() + .map(|c| Bytes48::from_bytes(c.as_ref()).expect("is right size")) + .collect(), + proofs: proofs + .into_iter() + .map(|p| Bytes48::from_bytes(p.as_ref()).expect("is right size")) + .collect(), + }; + + let finalized_payload = this.finalizer.process(block, fees).map(|mut payload| { + payload.extend_sidecars(vec![blob_sidecars]); + payload + }); Poll::Ready(finalized_payload) } } From b2d2bbbaabebe9905ae4130dcf643db5390745f1 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 15:46:53 -0600 Subject: [PATCH 06/14] bugfix: set appropriate fee recipients --- mev-build-rs/src/payload/builder_attributes.rs | 2 ++ mev-build-rs/src/payload/job.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mev-build-rs/src/payload/builder_attributes.rs b/mev-build-rs/src/payload/builder_attributes.rs index e08bbe55..08f737af 100644 --- a/mev-build-rs/src/payload/builder_attributes.rs +++ b/mev-build-rs/src/payload/builder_attributes.rs @@ -106,6 +106,8 @@ impl BuilderPayloadBuilderAttributes { if let Some(payload_id) = self.payload_id.take() { let id = mix_proposal_into_payload_id(payload_id, &proposal); self.inner.id = id; + // NOTE: direct all fee payments to builder + self.inner.suggested_fee_recipient = proposal.builder_fee_recipient; self.proposal = Some(proposal); } } diff --git a/mev-build-rs/src/payload/job.rs b/mev-build-rs/src/payload/job.rs index 7e635d8e..a03f8291 100644 --- a/mev-build-rs/src/payload/job.rs +++ b/mev-build-rs/src/payload/job.rs @@ -124,7 +124,7 @@ where payload_id: self.config.payload_id(), proposer_fee_recipient: attributes.proposer_fee_recipient, signer: attributes.builder_signer.clone(), - sender: Default::default(), + sender: attributes.builder_signer.address(), parent_hash: self.config.attributes.parent(), chain_id: self.config.chain_spec.chain().id(), cfg_env: self.config.initialized_cfg.clone(), From d6953ed2184c36fb0e2e125cd833a394cc387fa2 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 16:38:22 -0600 Subject: [PATCH 07/14] bugfix: adjust setting payment value --- mev-build-rs/src/payload/resolve.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs index 215111a5..cad821e8 100644 --- a/mev-build-rs/src/payload/resolve.rs +++ b/mev-build-rs/src/payload/resolve.rs @@ -172,7 +172,8 @@ impl PayloadFinalizer { // TODO: get amount to bid from bidder // - amount from block fees // - including any subsidy - fees + // TODO: remove temporary hardcoded subsidy + fees.max(U256::from(1337)) } fn prepare( @@ -185,7 +186,7 @@ impl PayloadFinalizer { let block = append_payment(&self.client, config, block, payment_amount)?; // TODO: - track proposer payment, revenue // TODO: ensure fees haven't changed - Ok(EthBuiltPayload::new(self.payload_id, block, fees)) + Ok(EthBuiltPayload::new(self.payload_id, block, payment_amount)) } fn process( From 050d82d4f50705df53777e6c91d2b673cbfe211c Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 16:46:11 -0600 Subject: [PATCH 08/14] hotfix: produce deneb blocks (no blobs) --- mev-build-rs/src/utils/compat.rs | 7 +++++-- mev-relay-rs/src/relay.rs | 16 +++++++++++++--- mev-rs/src/types/auction_contents.rs | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/mev-build-rs/src/utils/compat.rs b/mev-build-rs/src/utils/compat.rs index b9fe588a..85d6461b 100644 --- a/mev-build-rs/src/utils/compat.rs +++ b/mev-build-rs/src/utils/compat.rs @@ -1,5 +1,5 @@ use ethereum_consensus::{ - capella::mainnet as spec, + deneb::mainnet as spec, primitives::{Bytes32, ExecutionAddress}, ssz::{ prelude as ssz_rs, @@ -21,6 +21,7 @@ fn to_byte_vector(value: Bloom) -> ByteVector<256> { ByteVector::<256>::try_from(value.as_ref()).unwrap() } +// TODO: support multiple forks pub fn to_execution_payload(value: &SealedBlock) -> ExecutionPayload { let hash = value.hash(); let header = &value.header; @@ -58,6 +59,8 @@ pub fn to_execution_payload(value: &SealedBlock) -> ExecutionPayload { block_hash: to_bytes32(hash), transactions: TryFrom::try_from(transactions).unwrap(), withdrawals: TryFrom::try_from(withdrawals).unwrap(), + blob_gas_used: header.blob_gas_used.unwrap(), + excess_blob_gas: header.excess_blob_gas.unwrap(), }; - ExecutionPayload::Capella(payload) + ExecutionPayload::Deneb(payload) } diff --git a/mev-relay-rs/src/relay.rs b/mev-relay-rs/src/relay.rs index bc42e36e..f958e873 100644 --- a/mev-relay-rs/src/relay.rs +++ b/mev-relay-rs/src/relay.rs @@ -20,7 +20,7 @@ use mev_rs::{ verify_signed_data, }, types::{ - builder_bid, AuctionContents, AuctionRequest, BidTrace, BuilderBid, ExecutionPayload, + self, builder_bid, AuctionContents, AuctionRequest, BidTrace, BuilderBid, ExecutionPayload, ExecutionPayloadHeader, ProposerSchedule, SignedBidSubmission, SignedBlindedBeaconBlock, SignedBuilderBid, SignedValidatorRegistration, }, @@ -436,7 +436,13 @@ impl Relay { value, public_key: self.public_key.clone(), }), - Fork::Deneb => unimplemented!(), + Fork::Deneb => BuilderBid::Deneb(builder_bid::deneb::BuilderBid { + header, + // TODO: support blobs + blob_kzg_commitments: Default::default(), + value, + public_key: self.public_key.clone(), + }), _ => unreachable!("this fork is not reachable from this type"), }; let signature = sign_builder_message(&bid, &self.secret_key, &self.context)?; @@ -568,7 +574,11 @@ impl BlindedBlockProvider for Relay { let auction_contents = match local_payload.version() { Fork::Bellatrix => AuctionContents::Bellatrix(local_payload.clone()), Fork::Capella => AuctionContents::Capella(local_payload.clone()), - Fork::Deneb => unimplemented!(), + Fork::Deneb => AuctionContents::Deneb(types::deneb::AuctionContents { + execution_payload: local_payload.clone(), + // TODO: support blobs + blobs_bundle: Default::default(), + }), _ => unreachable!("fork not reachable from type"), }; Ok(auction_contents) diff --git a/mev-rs/src/types/auction_contents.rs b/mev-rs/src/types/auction_contents.rs index acccca85..4769fb07 100644 --- a/mev-rs/src/types/auction_contents.rs +++ b/mev-rs/src/types/auction_contents.rs @@ -21,7 +21,7 @@ pub mod deneb { ssz::prelude::*, }; - #[derive(Debug)] + #[derive(Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BlobsBundle { commitments: List, From 1e4938730dd3abaa46b9c5cb990d365a49a3764b Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 17:13:18 -0600 Subject: [PATCH 09/14] bugfix: correctly append receipt --- mev-build-rs/src/payload/resolve.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs index cad821e8..30120b98 100644 --- a/mev-build-rs/src/payload/resolve.rs +++ b/mev-build-rs/src/payload/resolve.rs @@ -12,7 +12,7 @@ use reth::{ kzg::{Blob, Bytes48}, proofs, revm::env::tx_env_with_recovered, - Address, BlobTransactionSidecar, Block, ChainId, Receipt, SealedBlock, Signature, + Address, BlobTransactionSidecar, Block, ChainId, Receipt, Receipts, SealedBlock, Signature, Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, B256, U256, }, @@ -119,15 +119,18 @@ fn append_payment( cumulative_gas_used, logs: result.into_logs().into_iter().map(Into::into).collect(), }; - // TODO skip clone here - let mut receipts = bundle_state_with_receipts.receipts().clone(); - receipts.push(vec![Some(receipt)]); body.push(payment_tx.into_signed()); db.merge_transitions(BundleRetention::PlainState); + // TODO skip clone here let block_number = header.number; + let mut receipts = bundle_state_with_receipts.receipts_by_block(block_number).to_vec(); + receipts.push(Some(receipt)); + + let receipts = Receipts::from_vec(vec![receipts]); + let bundle = BundleStateWithReceipts::new(db.take_bundle(), receipts, block_number); let receipts_root = bundle.receipts_root_slow(block_number).expect("Number is in range"); From 8ce54b55005d996d9552e73868dc9e2a0e302c3d Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 17:14:40 -0600 Subject: [PATCH 10/14] remove network check that fails on local testnets --- mev-build-rs/src/service.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index 55de1152..2ec2b8f9 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -110,11 +110,7 @@ pub async fn launch( network: Network, config: Config, ) -> eyre::Result<()> { - let chain_spec = &node_builder.config().chain; - let chain_name = chain_spec.chain.to_string(); - if chain_name != network.to_string() { - return Err(eyre::eyre!("configuration file did not match CLI")) - } + // TODO: verify network matches b/t reth and config? // TODO: ability to just run reth From ec8b827f2498ab097201cae8aea141d4ef74f585 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 17:15:27 -0600 Subject: [PATCH 11/14] log payload submission --- mev-build-rs/src/auctioneer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mev-build-rs/src/auctioneer.rs b/mev-build-rs/src/auctioneer.rs index 84a3fb0a..fb5b3ec0 100644 --- a/mev-build-rs/src/auctioneer.rs +++ b/mev-build-rs/src/auctioneer.rs @@ -165,6 +165,7 @@ impl Auctioneer { async fn submit_payload(&self, payload: EthBuiltPayload) { let auction = self.open_auctions.get(&payload.id()).expect("has auction"); + info!(?auction, ?payload, "submitting payload"); // TODO: should convert to ExecutionPayloadV3 etc. for blobs etc. match prepare_submission( payload, From f071d56290c166ffc1669bcc1eb564b40812ad99 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 17:29:11 -0600 Subject: [PATCH 12/14] bugfix: deadlock with channel usage --- mev-build-rs/src/builder.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mev-build-rs/src/builder.rs b/mev-build-rs/src/builder.rs index 0c9fbe8f..3e1dff99 100644 --- a/mev-build-rs/src/builder.rs +++ b/mev-build-rs/src/builder.rs @@ -210,7 +210,16 @@ impl< loop { tokio::select! { Some(message) = self.msgs.recv() => self.dispatch(message).await, - Some(Ok(Events::Attributes(attributes))) = payload_events.next() => self.on_payload_attributes(attributes).await, + Some(event) = payload_events.next() => match event { + Ok(event) => { + if let Events::Attributes(attributes) = event { + self.on_payload_attributes(attributes).await; + } + } + Err(err) => { + warn!(%err, "error getting payload events"); + } + } } } } From 844157a11a3ded1478b908430de7243dbbbe7a4c Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 17:36:22 -0600 Subject: [PATCH 13/14] clean up logs; add TODO comment --- mev-build-rs/src/auctioneer.rs | 9 ++++++++- mev-build-rs/src/service.rs | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mev-build-rs/src/auctioneer.rs b/mev-build-rs/src/auctioneer.rs index fb5b3ec0..5517601e 100644 --- a/mev-build-rs/src/auctioneer.rs +++ b/mev-build-rs/src/auctioneer.rs @@ -165,7 +165,14 @@ impl Auctioneer { async fn submit_payload(&self, payload: EthBuiltPayload) { let auction = self.open_auctions.get(&payload.id()).expect("has auction"); - info!(?auction, ?payload, "submitting payload"); + info!( + slot = auction.slot, + block_number = payload.block().number, + block_hash = %payload.block().hash(), + value = %payload.fees(), + relays=?auction.relays, + "submitting payload" + ); // TODO: should convert to ExecutionPayloadV3 etc. for blobs etc. match prepare_submission( payload, diff --git a/mev-build-rs/src/service.rs b/mev-build-rs/src/service.rs index 2ec2b8f9..5c2bd06b 100644 --- a/mev-build-rs/src/service.rs +++ b/mev-build-rs/src/service.rs @@ -114,6 +114,9 @@ pub async fn launch( // TODO: ability to just run reth + // TODO: consider blocking until we are synced... seems to be causing some kind of race + // condition upon launch + let payload_builder = PayloadServiceBuilder { extra_data: config.builder.extra_data.clone() }; let handle = node_builder From aa78177418a77335aa50172d792ccd0681681cfd Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 26 Apr 2024 18:00:40 -0600 Subject: [PATCH 14/14] hardcode temporary subsidy impl --- mev-build-rs/src/payload/resolve.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mev-build-rs/src/payload/resolve.rs b/mev-build-rs/src/payload/resolve.rs index 30120b98..8d9fae0d 100644 --- a/mev-build-rs/src/payload/resolve.rs +++ b/mev-build-rs/src/payload/resolve.rs @@ -176,7 +176,7 @@ impl PayloadFinalizer { // - amount from block fees // - including any subsidy // TODO: remove temporary hardcoded subsidy - fees.max(U256::from(1337)) + fees + U256::from(1337) } fn prepare(