Skip to content

Commit

Permalink
refactor payment machinery to builder and allow flexible payload fee …
Browse files Browse the repository at this point in the history
…recipient
  • Loading branch information
ralexstokes committed Apr 28, 2024
1 parent 16f9f21 commit 397cf56
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 210 deletions.
5 changes: 3 additions & 2 deletions example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ relays = [
"https://0x845bd072b7cd566f02faeb0a4033ce9399e42839ced64e8b2adcfc859ed1e8e1a5a293336a49feac6d9a5edb779be53a@boost-relay-sepolia.flashbots.net",
]
[builder.builder]
# address to collect transaction fees
fee_recipient = "0x0000000000000000000000000000000000000000"
# [optional] address to collect transaction fees
# if missing, sender from `execution_mnemonic` is used
fee_recipient = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
# [optional] extra data to write into built execution payload
extra_data = "0x68656C6C6F20776F726C640A" # "hello world"
# wallet seed for builder to author payment transactions
Expand Down
32 changes: 3 additions & 29 deletions mev-build-rs/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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,
};
Expand All @@ -25,13 +24,9 @@ use tracing::{error, warn};

fn make_attributes_for_proposer(
attributes: &BuilderPayloadBuilderAttributes,
builder_fee_recipient: Address,
builder_signer: Arc<LocalWallet>,
proposer: &Proposer,
) -> BuilderPayloadBuilderAttributes {
let proposal = ProposalAttributes {
builder_fee_recipient,
builder_signer,
proposer_gas_limit: proposer.gas_limit,
proposer_fee_recipient: proposer.fee_recipient,
};
Expand All @@ -50,7 +45,7 @@ pub enum Message {

#[derive(Deserialize, Debug, Default, Clone)]
pub struct Config {
pub fee_recipient: Address,
pub fee_recipient: Option<Address>,
pub genesis_time: Option<u64>,
pub extra_data: Option<Bytes>,
pub execution_mnemonic: String,
Expand All @@ -66,10 +61,8 @@ pub struct Builder<
auctioneer: Sender<AuctioneerMessage>,
payload_builder: PayloadBuilderHandle<Engine>,
payload_store: PayloadStore<Engine>,
config: Config,
context: Arc<Context>,
genesis_time: u64,
signer: Arc<LocalWallet>,
}

impl<
Expand All @@ -83,25 +76,11 @@ impl<
msgs: Receiver<Message>,
auctioneer: Sender<AuctioneerMessage>,
payload_builder: PayloadBuilderHandle<Engine>,
config: Config,
context: Arc<Context>,
genesis_time: u64,
) -> Self {
let payload_store = payload_builder.clone().into();
let signer = MnemonicBuilder::<English>::default()
.phrase(&config.execution_mnemonic)
.build()
.expect("is valid");
Self {
msgs,
auctioneer,
payload_builder,
payload_store,
config,
context,
genesis_time,
signer: Arc::new(signer),
}
Self { msgs, auctioneer, payload_builder, payload_store, context, genesis_time }
}

pub async fn process_proposals(
Expand All @@ -114,12 +93,7 @@ impl<

if let Some(proposals) = proposals {
for (proposer, relays) in proposals {
let attributes = make_attributes_for_proposer(
&attributes,
self.config.fee_recipient,
self.signer.clone(),
&proposer,
);
let attributes = make_attributes_for_proposer(&attributes, &proposer);

if self.start_build(&attributes).await.is_some() {
// TODO: can likely skip full attributes in `AuctionContext`
Expand Down
3 changes: 3 additions & 0 deletions mev-build-rs/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloy_signer_wallet::WalletError;
use ethereum_consensus::Error as ConsensusError;
use reth::payload::error::PayloadBuilderError;
use thiserror::Error;
Expand All @@ -8,4 +9,6 @@ pub enum Error {
Consensus(#[from] ConsensusError),
#[error(transparent)]
PayloadBuilderError(#[from] PayloadBuilderError),
#[error(transparent)]
WalletError(#[from] WalletError),
}
5 changes: 3 additions & 2 deletions mev-build-rs/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ pub struct BuilderNode;

impl BuilderNode {
/// Returns a [ComponentsBuilder] configured for a regular Ethereum node.
pub fn components<Node>(
pub fn components_with<Node>(
payload_service_builder: PayloadServiceBuilder,
) -> ComponentsBuilder<Node, EthereumPoolBuilder, PayloadServiceBuilder, EthereumNetworkBuilder>
where
Node: FullNodeTypes<Engine = BuilderEngineTypes>,
{
ComponentsBuilder::default()
.node_types::<Node>()
.pool(EthereumPoolBuilder::default())
.payload(PayloadServiceBuilder::default())
.payload(payload_service_builder)
.network(EthereumNetworkBuilder::default())
}
}
Expand Down
168 changes: 162 additions & 6 deletions mev-build-rs/src/payload/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::payload::builder_attributes::BuilderPayloadBuilderAttributes;
use crate::payload::{
builder_attributes::BuilderPayloadBuilderAttributes, resolve::PayloadFinalizerConfig,
};
use alloy_signer::SignerSync;
use alloy_signer_wallet::LocalWallet;
use reth::{
api::PayloadBuilderAttributes,
payload::{error::PayloadBuilderError, EthBuiltPayload, PayloadId},
Expand All @@ -9,7 +13,9 @@ use reth::{
eip4844::calculate_excess_blob_gas,
proofs,
revm::env::tx_env_with_recovered,
Block, Header, IntoRecoveredTransaction, Receipt, Receipts, EMPTY_OMMER_ROOT_HASH, U256,
Address, Block, ChainId, Header, IntoRecoveredTransaction, Receipt, Receipts, SealedBlock,
Signature, Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered,
TxEip1559, EMPTY_OMMER_ROOT_HASH, U256,
},
providers::{BundleStateWithReceipts, StateProviderFactory},
revm::{
Expand All @@ -32,7 +38,120 @@ use std::{
};
use tracing::{debug, trace, warn};

#[derive(Debug, Clone, Default)]
pub const BASE_TX_GAS_LIMIT: u64 = 21000;

fn make_payment_transaction(
signer: &LocalWallet,
config: &PayloadFinalizerConfig,
chain_id: ChainId,
nonce: u64,
max_fee_per_gas: u128,
value: U256,
) -> Result<TransactionSignedEcRecovered, PayloadBuilderError> {
let tx = Transaction::Eip1559(TxEip1559 {
chain_id,
nonce,
gas_limit: BASE_TX_GAS_LIMIT,
max_fee_per_gas,
max_priority_fee_per_gas: 0,
to: TransactionKind::Call(config.proposer_fee_recipient),
value,
access_list: Default::default(),
input: Default::default(),
});
let signature_hash = tx.signature_hash();
let signature = 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, signer.address()))
}

fn append_payment<Client: StateProviderFactory>(
client: Client,
bundle_state_with_receipts: BundleStateWithReceipts,
signer: &LocalWallet,
config: &PayloadFinalizerConfig,
chain_id: ChainId,
block: SealedBlock,
value: U256,
) -> Result<SealedBlock, PayloadBuilderError> {
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)
// TODO skip clone here...
.with_bundle_prestate(bundle_state_with_receipts.state().clone())
.with_bundle_update()
.build();

let signer_account = db.load_cache_account(signer.address())?;
// TODO handle option
let nonce = signer_account.account_info().expect("account exists").nonce;
// TODO handle option
// SAFETY: cast to bigger type always succeeds
let max_fee_per_gas = block.header().base_fee_per_gas.expect("exists") as u128;
let payment_tx =
make_payment_transaction(signer, config, chain_id, nonce, max_fee_per_gas, value)?;

// TODO: skip clones 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
// 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(),
};

body.push(payment_tx.into_signed());

db.merge_transitions(BundleRetention::PlainState);

let block_number = header.number;
// TODO skip clone here
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");
let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range");
let state_root = state_provider.state_root(bundle.state())?;
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, Clone)]
pub struct PayloadBuilder(Arc<Inner>);

impl Deref for PayloadBuilder {
Expand All @@ -43,16 +162,53 @@ impl Deref for PayloadBuilder {
}
}

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Inner {
states: Arc<Mutex<HashMap<PayloadId, BundleStateWithReceipts>>>,
pub signer: LocalWallet,
pub fee_recipient: Address,
pub chain_id: ChainId,
pub states: Mutex<HashMap<PayloadId, BundleStateWithReceipts>>,
}

impl Inner {
impl PayloadBuilder {
pub fn new(signer: LocalWallet, fee_recipient: Address, chain_id: ChainId) -> Self {
let inner = Inner { signer, fee_recipient, chain_id, states: Default::default() };
Self(Arc::new(inner))
}

pub fn get_build_state(&self, payload_id: PayloadId) -> Option<BundleStateWithReceipts> {
let mut state = self.states.lock().expect("can lock");
state.remove(&payload_id)
}

fn determine_payment_amount(&self, fees: U256) -> U256 {
// TODO: remove temporary hardcoded subsidy
fees + U256::from(1337)
}

pub fn finalize_payload<Client: StateProviderFactory>(
&self,
payload_id: PayloadId,
client: Client,
block: SealedBlock,
fees: U256,
config: &PayloadFinalizerConfig,
) -> Result<EthBuiltPayload, PayloadBuilderError> {
let payment_amount = self.determine_payment_amount(fees);
let bundle_state_with_receipts = self
.get_build_state(payload_id)
.ok_or_else(|| PayloadBuilderError::Other("missing build state for payload".into()))?;
let block = append_payment(
client,
bundle_state_with_receipts,
&self.signer,
config,
self.chain_id,
block,
payment_amount,
)?;
Ok(EthBuiltPayload::new(payload_id, block, payment_amount))
}
}

impl<Pool, Client> reth_basic_payload_builder::PayloadBuilder<Pool, Client> for PayloadBuilder
Expand Down
10 changes: 1 addition & 9 deletions mev-build-rs/src/payload/builder_attributes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use alloy_signer_wallet::LocalWallet;
use reth::{
api::PayloadBuilderAttributes,
payload::{EthPayloadBuilderAttributes, PayloadId},
Expand All @@ -12,7 +11,7 @@ use reth::{
},
};
use sha2::Digest;
use std::{convert::Infallible, sync::Arc};
use std::convert::Infallible;

pub fn payload_id_with_bytes(
parent: &B256,
Expand Down Expand Up @@ -51,7 +50,6 @@ pub fn mix_proposal_into_payload_id(
let mut hasher = sha2::Sha256::new();
hasher.update(payload_id);

hasher.update(proposal.builder_fee_recipient.as_slice());
hasher.update(proposal.proposer_gas_limit.to_be_bytes());
hasher.update(proposal.proposer_fee_recipient.as_slice());

Expand All @@ -61,10 +59,6 @@ pub fn mix_proposal_into_payload_id(

#[derive(Debug, Clone)]
pub struct ProposalAttributes {
// TODO: move to payload builder
pub builder_fee_recipient: Address,
// TODO: move to payload builder
pub builder_signer: Arc<LocalWallet>,
pub proposer_gas_limit: u64,
pub proposer_fee_recipient: Address,
}
Expand Down Expand Up @@ -108,8 +102,6 @@ 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);
}
}
Expand Down
6 changes: 1 addition & 5 deletions mev-build-rs/src/payload/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,16 @@ 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: 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(),
block_env: self.config.initialized_block_env.clone(),
builder: self.builder.clone(),
});
let finalizer = PayloadFinalizer {
client: self.client.clone(),
_pool: self.pool.clone(),
payload_id: self.config.payload_id(),
builder: self.builder.clone(),
config,
};

Expand Down
Loading

0 comments on commit 397cf56

Please sign in to comment.