reth_ethereum_payload_builder/
lib.rs
1#![doc(
4 html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5 html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6 issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
10#![allow(clippy::useless_let_if_seq)]
11
12pub mod validator;
13pub use validator::EthereumExecutionPayloadValidator;
14
15use alloy_consensus::{Transaction, Typed2718};
16use alloy_primitives::U256;
17use reth_basic_payload_builder::{
18 is_better_payload, BuildArguments, BuildOutcome, PayloadBuilder, PayloadConfig,
19};
20use reth_chainspec::{ChainSpec, ChainSpecProvider, EthChainSpec, EthereumHardforks};
21use reth_errors::{BlockExecutionError, BlockValidationError};
22use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
23use reth_evm::{
24 execute::{BlockBuilder, BlockBuilderOutcome},
25 ConfigureEvm, Evm, NextBlockEnvAttributes,
26};
27use reth_evm_ethereum::EthEvmConfig;
28use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes};
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_primitives_traits::SignedTransaction;
32use reth_revm::{database::StateProviderDatabase, db::State};
33use reth_storage_api::StateProviderFactory;
34use reth_transaction_pool::{
35 error::InvalidPoolTransactionError, BestTransactions, BestTransactionsAttributes,
36 PoolTransaction, TransactionPool, ValidPoolTransaction,
37};
38use revm::context_interface::Block as _;
39use std::sync::Arc;
40use tracing::{debug, trace, warn};
41
42mod config;
43pub use config::*;
44use reth_primitives_traits::transaction::error::InvalidTransactionError;
45use reth_transaction_pool::error::Eip4844PoolTransactionError;
46
47type BestTransactionsIter<Pool> = Box<
48 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
49>;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
54 client: Client,
56 pool: Pool,
58 evm_config: EvmConfig,
60 builder_config: EthereumBuilderConfig,
62}
63
64impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
65 pub const fn new(
67 client: Client,
68 pool: Pool,
69 evm_config: EvmConfig,
70 builder_config: EthereumBuilderConfig,
71 ) -> Self {
72 Self { client, pool, evm_config, builder_config }
73 }
74}
75
76impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
78where
79 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
80 Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec> + Clone,
81 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
82{
83 type Attributes = EthPayloadBuilderAttributes;
84 type BuiltPayload = EthBuiltPayload;
85
86 fn try_build(
87 &self,
88 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
89 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
90 default_ethereum_payload(
91 self.evm_config.clone(),
92 self.client.clone(),
93 self.pool.clone(),
94 self.builder_config.clone(),
95 args,
96 |attributes| self.pool.best_transactions_with_attributes(attributes),
97 )
98 }
99
100 fn build_empty_payload(
101 &self,
102 config: PayloadConfig<Self::Attributes>,
103 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
104 let args = BuildArguments::new(Default::default(), config, Default::default(), None);
105
106 default_ethereum_payload(
107 self.evm_config.clone(),
108 self.client.clone(),
109 self.pool.clone(),
110 self.builder_config.clone(),
111 args,
112 |attributes| self.pool.best_transactions_with_attributes(attributes),
113 )?
114 .into_payload()
115 .ok_or_else(|| PayloadBuilderError::MissingPayload)
116 }
117}
118
119#[inline]
125pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
126 evm_config: EvmConfig,
127 client: Client,
128 pool: Pool,
129 builder_config: EthereumBuilderConfig,
130 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
131 best_txs: F,
132) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
133where
134 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
135 Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
136 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
137 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
138{
139 let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
140 let PayloadConfig { parent_header, attributes } = config;
141
142 let state_provider = client.state_by_block_hash(parent_header.hash())?;
143 let state = StateProviderDatabase::new(&state_provider);
144 let mut db =
145 State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
146
147 let mut builder = evm_config
148 .builder_for_next_block(
149 &mut db,
150 &parent_header,
151 NextBlockEnvAttributes {
152 timestamp: attributes.timestamp(),
153 suggested_fee_recipient: attributes.suggested_fee_recipient(),
154 prev_randao: attributes.prev_randao(),
155 gas_limit: builder_config.gas_limit(parent_header.gas_limit),
156 parent_beacon_block_root: attributes.parent_beacon_block_root(),
157 withdrawals: Some(attributes.withdrawals().clone()),
158 },
159 )
160 .map_err(PayloadBuilderError::other)?;
161
162 let chain_spec = client.chain_spec();
163
164 debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
165 let mut cumulative_gas_used = 0;
166 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit;
167 let base_fee = builder.evm_mut().block().basefee;
168
169 let mut best_txs = best_txs(BestTransactionsAttributes::new(
170 base_fee,
171 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
172 ));
173 let mut total_fees = U256::ZERO;
174
175 builder.apply_pre_execution_changes().map_err(|err| {
176 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
177 PayloadBuilderError::Internal(err.into())
178 })?;
179
180 let mut block_blob_count = 0;
181 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
182 let max_blob_count =
183 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_default();
184
185 while let Some(pool_tx) = best_txs.next() {
186 if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
188 best_txs.mark_invalid(
192 &pool_tx,
193 InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
194 );
195 continue
196 }
197
198 if cancel.is_cancelled() {
200 return Ok(BuildOutcome::Cancelled)
201 }
202
203 let tx = pool_tx.to_consensus();
205
206 if let Some(blob_tx) = tx.as_eip4844() {
209 let tx_blob_count = blob_tx.blob_versioned_hashes.len() as u64;
210
211 if block_blob_count + tx_blob_count > max_blob_count {
212 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
217 best_txs.mark_invalid(
218 &pool_tx,
219 InvalidPoolTransactionError::Eip4844(
220 Eip4844PoolTransactionError::TooManyEip4844Blobs {
221 have: block_blob_count + tx_blob_count,
222 permitted: max_blob_count,
223 },
224 ),
225 );
226 continue
227 }
228 }
229
230 let gas_used = match builder.execute_transaction(tx.clone()) {
231 Ok(gas_used) => gas_used,
232 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
233 error, ..
234 })) => {
235 if error.is_nonce_too_low() {
236 trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
238 } else {
239 trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
242 best_txs.mark_invalid(
243 &pool_tx,
244 InvalidPoolTransactionError::Consensus(
245 InvalidTransactionError::TxTypeNotSupported,
246 ),
247 );
248 }
249 continue
250 }
251 Err(err) => return Err(PayloadBuilderError::evm(err)),
253 };
254
255 if let Some(blob_tx) = tx.as_eip4844() {
257 block_blob_count += blob_tx.blob_versioned_hashes.len() as u64;
258
259 if block_blob_count == max_blob_count {
261 best_txs.skip_blobs();
262 }
263 }
264
265 let miner_fee =
267 tx.effective_tip_per_gas(base_fee).expect("fee is always valid; execution succeeded");
268 total_fees += U256::from(miner_fee) * U256::from(gas_used);
269 cumulative_gas_used += gas_used;
270 }
271
272 if !is_better_payload(best_payload.as_ref(), total_fees) {
274 drop(builder);
276 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
278 }
279
280 let BlockBuilderOutcome { execution_result, block, .. } = builder.finish(&state_provider)?;
281
282 let requests = chain_spec
283 .is_prague_active_at_timestamp(attributes.timestamp)
284 .then_some(execution_result.requests);
285
286 let mut blob_sidecars = Vec::new();
288
289 if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp) {
291 blob_sidecars = pool
293 .get_all_blobs_exact(
294 block
295 .body()
296 .transactions()
297 .filter(|tx| tx.is_eip4844())
298 .map(|tx| *tx.tx_hash())
299 .collect(),
300 )
301 .map_err(PayloadBuilderError::other)?;
302 }
303
304 let sealed_block = Arc::new(block.sealed_block().clone());
305 debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
306
307 let mut payload = EthBuiltPayload::new(attributes.id, sealed_block, total_fees, requests);
308
309 payload.extend_sidecars(blob_sidecars.into_iter().map(Arc::unwrap_or_clone));
311
312 Ok(BuildOutcome::Better { payload, cached_reads })
313}