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))]
10
11use alloy_consensus::Transaction;
12use alloy_primitives::{Bytes, U256};
13use alloy_rlp::Encodable;
14use alloy_rpc_types_engine::PayloadAttributes as EthPayloadAttributes;
15use reth_basic_payload_builder::{
16 is_better_payload, BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder,
17 PayloadConfig,
18};
19use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
20use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
21use reth_errors::{BlockExecutionError, BlockValidationError, ConsensusError};
22use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
23use reth_evm::{
24 block::TxResult,
25 execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
26 ConfigureEvm, Evm, NextBlockEnvAttributes,
27};
28use reth_evm_ethereum::EthEvmConfig;
29use reth_execution_cache::{CachedStateMetrics, CachedStateMetricsSource, CachedStateProvider};
30use reth_payload_builder::{BlobSidecars, EthBuiltPayload};
31use reth_payload_builder_primitives::PayloadBuilderError;
32use reth_payload_primitives::PayloadAttributes;
33use reth_primitives_traits::transaction::error::InvalidTransactionError;
34use reth_revm::{database::StateProviderDatabase, db::State};
35use reth_storage_api::StateProviderFactory;
36use reth_transaction_pool::{
37 error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
38 BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
39 ValidPoolTransaction,
40};
41use revm::context_interface::{Block as _, Cfg as _};
42use std::sync::Arc;
43use tracing::{debug, trace, warn};
44
45mod config;
46pub use config::*;
47
48pub mod validator;
49pub use validator::EthereumExecutionPayloadValidator;
50
51type BestTransactionsIter<Pool> = Box<
52 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
53>;
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
58 client: Client,
60 pool: Pool,
62 evm_config: EvmConfig,
64 builder_config: EthereumBuilderConfig,
66}
67
68impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
69 pub const fn new(
71 client: Client,
72 pool: Pool,
73 evm_config: EvmConfig,
74 builder_config: EthereumBuilderConfig,
75 ) -> Self {
76 Self { client, pool, evm_config, builder_config }
77 }
78}
79
80impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
82where
83 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
84 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
85 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
86{
87 type Attributes = EthPayloadAttributes;
88 type BuiltPayload = EthBuiltPayload;
89
90 fn try_build(
91 &self,
92 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
93 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
94 default_ethereum_payload(
95 self.evm_config.clone(),
96 self.client.clone(),
97 self.pool.clone(),
98 self.builder_config.clone(),
99 args,
100 |attributes| self.pool.best_transactions_with_attributes(attributes),
101 )
102 }
103
104 fn on_missing_payload(
105 &self,
106 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
107 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
108 if self.builder_config.await_payload_on_missing {
109 MissingPayloadBehaviour::AwaitInProgress
110 } else {
111 MissingPayloadBehaviour::RaceEmptyPayload
112 }
113 }
114
115 fn build_empty_payload(
116 &self,
117 config: PayloadConfig<Self::Attributes>,
118 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
119 let args = BuildArguments::new(
120 Default::default(),
121 Default::default(),
122 None,
123 config,
124 Default::default(),
125 None,
126 );
127
128 default_ethereum_payload(
129 self.evm_config.clone(),
130 self.client.clone(),
131 self.pool.clone(),
132 self.builder_config.clone(),
133 args,
134 |_| -> BestTransactionsIter<Pool> { Box::new(std::iter::empty()) },
135 )?
136 .into_payload()
137 .ok_or_else(|| PayloadBuilderError::MissingPayload)
138 }
139}
140
141#[inline]
147pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
148 evm_config: EvmConfig,
149 client: Client,
150 pool: Pool,
151 builder_config: EthereumBuilderConfig,
152 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
153 best_txs: F,
154) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
155where
156 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
157 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
158 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
159 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
160{
161 let BuildArguments {
162 mut cached_reads,
163 execution_cache,
164 trie_handle,
165 config,
166 cancel,
167 best_payload,
168 } = args;
169 let PayloadConfig { parent_header, attributes, payload_id } = config;
170
171 let mut state_provider = client.state_by_block_hash(parent_header.hash())?;
172 if let Some(execution_cache) = execution_cache {
173 state_provider = Box::new(CachedStateProvider::new(
174 state_provider,
175 execution_cache.cache().clone(),
176 CachedStateMetrics::zeroed(CachedStateMetricsSource::Builder),
179 ));
180 }
181 let state = StateProviderDatabase::new(state_provider.as_ref());
182 let chain_spec = client.chain_spec();
183 let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(attributes.timestamp());
184 let mut db = State::builder()
185 .with_database(cached_reads.as_db_mut(state))
186 .with_bundle_update()
187 .with_bal_builder_if(is_amsterdam)
188 .build();
189
190 let mut builder = evm_config
191 .builder_for_next_block(
192 &mut db,
193 &parent_header,
194 NextBlockEnvAttributes {
195 timestamp: attributes.timestamp(),
196 suggested_fee_recipient: attributes.suggested_fee_recipient,
197 prev_randao: attributes.prev_randao,
198 gas_limit: builder_config.gas_limit(parent_header.gas_limit),
199 parent_beacon_block_root: attributes.parent_beacon_block_root(),
200 withdrawals: attributes.withdrawals.clone().map(Into::into),
201 extra_data: builder_config.extra_data,
202 slot_number: attributes.slot_number(),
203 },
204 )
205 .map_err(PayloadBuilderError::other)?;
206
207 debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
208 let mut cumulative_tx_gas_used = 0;
209 let mut block_regular_gas_used = 0;
210 let mut block_state_gas_used = 0;
211 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
212 let tx_gas_limit_cap = builder.evm_mut().cfg_env().tx_gas_limit_cap();
213 let base_fee = builder.evm_mut().block().basefee();
214
215 let mut best_txs = best_txs(BestTransactionsAttributes::new(
216 base_fee,
217 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
218 ));
219 let mut total_fees = U256::ZERO;
220
221 if let Some(ref handle) = trie_handle {
224 builder.executor_mut().set_state_hook(Some(Box::new(handle.state_hook())));
225 }
226
227 builder.apply_pre_execution_changes().map_err(|err| {
228 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
229 PayloadBuilderError::Internal(err.into())
230 })?;
231
232 let mut blob_sidecars = BlobSidecars::Empty;
235
236 let mut block_blob_count = 0;
237 let mut block_transactions_rlp_length = 0;
238
239 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
240 let protocol_max_blob_count =
241 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_else(Default::default);
242
243 let max_blob_count = builder_config
246 .max_blobs_per_block
247 .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
248 .unwrap_or(protocol_max_blob_count);
249
250 let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
251
252 let withdrawals_rlp_length =
253 attributes.withdrawals.as_ref().map(|withdrawals| withdrawals.length()).unwrap_or(0);
254
255 while let Some(pool_tx) = best_txs.next() {
256 let exceeds_gas_limit = if is_amsterdam {
258 let regular_available_gas = block_gas_limit.saturating_sub(block_regular_gas_used);
259 let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
260 let regular_tx_gas_limit = pool_tx.gas_limit().min(tx_gas_limit_cap);
261
262 if regular_tx_gas_limit > regular_available_gas {
263 Some((regular_tx_gas_limit, regular_available_gas))
264 } else if pool_tx.gas_limit() > state_available_gas {
265 Some((pool_tx.gas_limit(), state_available_gas))
266 } else {
267 None
268 }
269 } else {
270 let block_available_gas = block_gas_limit.saturating_sub(cumulative_tx_gas_used);
271 (pool_tx.gas_limit() > block_available_gas)
272 .then_some((pool_tx.gas_limit(), block_available_gas))
273 };
274
275 if let Some((transaction_gas_limit, block_available_gas)) = exceeds_gas_limit {
276 best_txs.mark_invalid(
280 &pool_tx,
281 InvalidPoolTransactionError::ExceedsGasLimit(
282 transaction_gas_limit,
283 block_available_gas,
284 ),
285 );
286 continue
287 }
288
289 if cancel.is_cancelled() {
291 return Ok(BuildOutcome::Cancelled)
292 }
293
294 let tx = pool_tx.to_consensus();
296
297 let tx_rlp_len = tx.inner().length();
298
299 let estimated_block_size_with_tx =
300 block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
303 best_txs.mark_invalid(
304 &pool_tx,
305 InvalidPoolTransactionError::OversizedData {
306 size: estimated_block_size_with_tx,
307 limit: MAX_RLP_BLOCK_SIZE,
308 },
309 );
310 continue
311 }
312
313 let mut blob_tx_sidecar = None;
316 let tx_blob_count = tx.blob_count();
317
318 if let Some(tx_blob_count) = tx_blob_count {
319 if block_blob_count + tx_blob_count > max_blob_count {
320 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
325 best_txs.mark_invalid(
326 &pool_tx,
327 InvalidPoolTransactionError::Eip4844(
328 Eip4844PoolTransactionError::TooManyEip4844Blobs {
329 have: block_blob_count + tx_blob_count,
330 permitted: max_blob_count,
331 },
332 ),
333 );
334 continue
335 }
336
337 let blob_sidecar_result = 'sidecar: {
338 let Some(sidecar) =
339 pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
340 else {
341 break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
342 };
343
344 if is_osaka {
345 if sidecar.is_eip7594() {
346 Ok(sidecar)
347 } else {
348 Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
349 }
350 } else if sidecar.is_eip4844() {
351 Ok(sidecar)
352 } else {
353 Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
354 }
355 };
356
357 blob_tx_sidecar = match blob_sidecar_result {
358 Ok(sidecar) => Some(sidecar),
359 Err(error) => {
360 best_txs.mark_invalid(&pool_tx, InvalidPoolTransactionError::Eip4844(error));
361 continue
362 }
363 };
364 }
365
366 let miner_fee = tx.effective_tip_per_gas(base_fee);
367 let tx_hash = *tx.tx_hash();
368
369 let mut tx_regular_gas_used = 0;
370 let gas_output = match builder.execute_transaction_with_result_closure(tx, |result| {
371 tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
372 }) {
373 Ok(gas_output) => gas_output,
374 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
375 error, ..
376 })) => {
377 if error.is_nonce_too_low() {
378 trace!(target: "payload_builder", %error, ?tx_hash, "skipping nonce too low transaction");
380 } else {
381 trace!(target: "payload_builder", %error, ?tx_hash, "skipping invalid transaction and its descendants");
384 best_txs.mark_invalid(
385 &pool_tx,
386 InvalidPoolTransactionError::Consensus(
387 InvalidTransactionError::TxTypeNotSupported,
388 ),
389 );
390 }
391 continue
392 }
393 Err(BlockExecutionError::Validation(
396 BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
397 transaction_gas_limit,
398 block_available_gas,
399 },
400 )) => {
401 trace!(target: "payload_builder", %transaction_gas_limit, %block_available_gas, ?tx_hash, "skipping transaction exceeding block gas limit");
402 best_txs.mark_invalid(
403 &pool_tx,
404 InvalidPoolTransactionError::ExceedsGasLimit(
405 transaction_gas_limit,
406 block_available_gas,
407 ),
408 );
409 continue
410 }
411 Err(err) => return Err(PayloadBuilderError::evm(err)),
413 };
414
415 if let Some(blob_count) = tx_blob_count {
417 block_blob_count += blob_count;
418
419 if block_blob_count == max_blob_count {
421 best_txs.skip_blobs();
422 }
423 }
424
425 block_transactions_rlp_length += tx_rlp_len;
426
427 let gas_used = gas_output.tx_gas_used();
429 let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
430 total_fees += U256::from(miner_fee) * U256::from(gas_used);
431 cumulative_tx_gas_used += gas_used;
432 block_regular_gas_used += tx_regular_gas_used;
433 block_state_gas_used += gas_output.state_gas_used();
434
435 if let Some(sidecar) = blob_tx_sidecar {
437 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
438 }
439 }
440
441 if !is_better_payload(best_payload.as_ref(), total_fees) {
443 drop(builder);
445 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
447 }
448
449 let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if let Some(
450 mut handle,
451 ) = trie_handle
452 {
453 builder.executor_mut().set_state_hook(None);
456
457 match handle.state_root() {
461 Ok(outcome) => {
462 debug!(target: "payload_builder", id=%payload_id, state_root=?outcome.state_root, "received state root from sparse trie");
463 builder.finish(
464 state_provider.as_ref(),
465 Some((outcome.state_root, Arc::unwrap_or_clone(outcome.trie_updates))),
466 )?
467 }
468 Err(err) => {
469 warn!(target: "payload_builder", id=%payload_id, %err, "sparse trie failed, falling back to sync state root");
470 builder.finish(state_provider.as_ref(), None)?
471 }
472 }
473 } else {
474 builder.finish(state_provider.as_ref(), None)?
475 };
476
477 let requests = chain_spec
478 .is_prague_active_at_timestamp(attributes.timestamp)
479 .then_some(execution_result.requests);
480
481 let sealed_block = Arc::new(block.into_sealed_block());
482 debug!(target: "payload_builder", id=%payload_id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
483
484 if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE {
485 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
486 rlp_length: sealed_block.rlp_length(),
487 max_rlp_length: MAX_RLP_BLOCK_SIZE,
488 }));
489 }
490
491 let block_access_list: Option<Bytes> =
492 block_access_list.map(|block_access_list| alloy_rlp::encode(&block_access_list).into());
493 let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, block_access_list)
494 .with_sidecars(blob_sidecars);
496
497 Ok(BuildOutcome::Better { payload, cached_reads })
498}