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::{BlockHeader, Transaction};
12use alloy_primitives::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},
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::{EvmStateProvider, StateProvider, 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 mut state_root_handle,
165 config,
166 cancel,
167 best_payload,
168 } = args;
169 let PayloadConfig { parent_header, attributes, payload_id, .. } = config;
170 let skip_state_root = builder_config.skip_state_root;
171
172 let state_provider = client.state_by_block_hash(parent_header.hash())?;
173 let evm_state_provider = (&state_provider).into_evm_state_provider();
174 let cached_state_provider = execution_cache.map(|execution_cache| {
175 CachedStateProvider::new(
176 &evm_state_provider,
177 execution_cache.cache().clone(),
178 Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Builder)),
181 )
182 });
183 let state = StateProviderDatabase::new(
184 cached_state_provider
185 .as_ref()
186 .map(|provider| provider as &dyn EvmStateProvider)
187 .unwrap_or(&evm_state_provider),
188 );
189 let chain_spec = client.chain_spec();
190 let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(attributes.timestamp());
191 let mut db = State::builder()
192 .with_database(cached_reads.as_db_mut(state))
193 .with_bundle_update()
194 .with_bal_builder_if(is_amsterdam)
195 .build();
196
197 let evm_config = evm_config.with_jit_support();
198 let mut builder = evm_config
199 .builder_for_next_block(
200 &mut db,
201 &parent_header,
202 NextBlockEnvAttributes {
203 timestamp: attributes.timestamp(),
204 suggested_fee_recipient: attributes.suggested_fee_recipient,
205 prev_randao: attributes.prev_randao,
206 gas_limit: builder_config
207 .gas_limit_with_target(parent_header.gas_limit, attributes.target_gas_limit()),
208 parent_beacon_block_root: attributes.parent_beacon_block_root(),
209 withdrawals: attributes.withdrawals.clone().map(Into::into),
210 extra_data: builder_config.extra_data.clone(),
211 slot_number: attributes.slot_number(),
212 },
213 )
214 .map_err(PayloadBuilderError::other)?;
215
216 debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
217 let mut cumulative_tx_gas_used = 0;
218 let mut block_regular_gas_used = 0;
219 let mut block_state_gas_used = 0;
220 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
221 let tx_gas_limit_cap = builder.evm_mut().cfg_env().tx_gas_limit_cap();
222 let base_fee = builder.evm_mut().block().basefee();
223
224 let mut best_txs = best_txs(BestTransactionsAttributes::new(
225 base_fee,
226 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
227 ));
228 let mut total_fees = U256::ZERO;
229
230 if let Some(task) = state_root_handle.as_mut() {
232 builder.evm_mut().db_mut().set_state_hook(Some(Box::new(task.take_state_hook())));
233 }
234
235 builder.apply_pre_execution_changes().map_err(|err| {
236 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
237 PayloadBuilderError::Internal(err.into())
238 })?;
239
240 let mut blob_sidecars = BlobSidecars::Empty;
243
244 let mut block_blob_count = 0;
245 let mut block_transactions_rlp_length = 0;
246
247 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
248 let protocol_max_blob_count =
249 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_else(Default::default);
250
251 let max_blob_count = builder_config
254 .max_blobs_per_block
255 .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
256 .unwrap_or(protocol_max_blob_count);
257
258 let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
259
260 let withdrawals_rlp_length =
261 attributes.withdrawals.as_ref().map(|withdrawals| withdrawals.length()).unwrap_or(0);
262
263 while let Some(pool_tx) = best_txs.next() {
264 let exceeds_gas_limit = if is_amsterdam {
266 let regular_available_gas = block_gas_limit.saturating_sub(block_regular_gas_used);
267 let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
268 let regular_tx_gas_limit = pool_tx.gas_limit().min(tx_gas_limit_cap);
269
270 if regular_tx_gas_limit > regular_available_gas {
271 Some((regular_tx_gas_limit, regular_available_gas))
272 } else if pool_tx.gas_limit() > state_available_gas {
273 Some((pool_tx.gas_limit(), state_available_gas))
274 } else {
275 None
276 }
277 } else {
278 let block_available_gas = block_gas_limit.saturating_sub(cumulative_tx_gas_used);
279 (pool_tx.gas_limit() > block_available_gas)
280 .then_some((pool_tx.gas_limit(), block_available_gas))
281 };
282
283 if let Some((transaction_gas_limit, block_available_gas)) = exceeds_gas_limit {
284 best_txs.mark_invalid(
288 &pool_tx,
289 InvalidPoolTransactionError::ExceedsGasLimit(
290 transaction_gas_limit,
291 block_available_gas,
292 ),
293 );
294 continue
295 }
296
297 if cancel.is_cancelled() {
299 return Ok(BuildOutcome::Cancelled)
300 }
301
302 let tx = pool_tx.to_consensus();
304
305 let tx_rlp_len = tx.inner().length();
306
307 let estimated_block_size_with_tx =
308 block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
311 best_txs.mark_invalid(
312 &pool_tx,
313 InvalidPoolTransactionError::OversizedData {
314 size: estimated_block_size_with_tx,
315 limit: MAX_RLP_BLOCK_SIZE,
316 },
317 );
318 continue
319 }
320
321 let mut blob_tx_sidecar = None;
324 let tx_blob_count = tx.blob_count();
325
326 if let Some(tx_blob_count) = tx_blob_count {
327 if block_blob_count + tx_blob_count > max_blob_count {
328 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
333 best_txs.mark_invalid(
334 &pool_tx,
335 InvalidPoolTransactionError::Eip4844(
336 Eip4844PoolTransactionError::TooManyEip4844Blobs {
337 have: block_blob_count + tx_blob_count,
338 permitted: max_blob_count,
339 },
340 ),
341 );
342 continue
343 }
344
345 let blob_sidecar_result = 'sidecar: {
346 let Some(sidecar) =
347 pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
348 else {
349 break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
350 };
351
352 if is_osaka {
353 if sidecar.is_eip7594() {
354 Ok(sidecar)
355 } else {
356 Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
357 }
358 } else if sidecar.is_eip4844() {
359 Ok(sidecar)
360 } else {
361 Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
362 }
363 };
364
365 blob_tx_sidecar = match blob_sidecar_result {
366 Ok(sidecar) => Some(sidecar),
367 Err(error) => {
368 best_txs.mark_invalid(&pool_tx, InvalidPoolTransactionError::Eip4844(error));
369 continue
370 }
371 };
372 }
373
374 let miner_fee = tx.effective_tip_per_gas(base_fee);
375 let tx_hash = *tx.tx_hash();
376
377 let mut tx_regular_gas_used = 0;
378 let gas_output = match builder.execute_transaction_with_result_closure(tx, |result| {
379 tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
380 }) {
381 Ok(gas_output) => gas_output,
382 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
383 error, ..
384 })) => {
385 if error.is_nonce_too_low() {
386 trace!(target: "payload_builder", %error, ?tx_hash, "skipping nonce too low transaction");
388 } else {
389 trace!(target: "payload_builder", %error, ?tx_hash, "skipping invalid transaction and its descendants");
392 best_txs.mark_invalid(
393 &pool_tx,
394 InvalidPoolTransactionError::Consensus(
395 InvalidTransactionError::TxTypeNotSupported,
396 ),
397 );
398 }
399 continue
400 }
401 Err(BlockExecutionError::Validation(
404 BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
405 transaction_gas_limit,
406 block_available_gas,
407 },
408 )) => {
409 trace!(target: "payload_builder", %transaction_gas_limit, %block_available_gas, ?tx_hash, "skipping transaction exceeding block gas limit");
410 best_txs.mark_invalid(
411 &pool_tx,
412 InvalidPoolTransactionError::ExceedsGasLimit(
413 transaction_gas_limit,
414 block_available_gas,
415 ),
416 );
417 continue
418 }
419 Err(err) => return Err(PayloadBuilderError::evm(err)),
421 };
422
423 if let Some(blob_count) = tx_blob_count {
425 block_blob_count += blob_count;
426
427 if block_blob_count == max_blob_count {
429 best_txs.skip_blobs();
430 }
431 }
432
433 block_transactions_rlp_length += tx_rlp_len;
434
435 let gas_used = gas_output.tx_gas_used();
437 let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
438 total_fees += U256::from(miner_fee) * U256::from(gas_used);
439 cumulative_tx_gas_used += gas_used;
440 block_regular_gas_used += tx_regular_gas_used;
441 block_state_gas_used += gas_output.state_gas_used();
442
443 if let Some(sidecar) = blob_tx_sidecar {
445 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
446 }
447 }
448
449 if !is_better_payload(best_payload.as_ref(), total_fees) {
451 drop(builder);
453 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
455 }
456
457 let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if skip_state_root
458 {
459 debug!(
460 target: "payload_builder",
461 id = %payload_id,
462 state_root = ?parent_header.state_root(),
463 "skipping payload state-root computation"
464 );
465 builder.finish(
466 state_provider.as_ref(),
467 Some((parent_header.state_root(), Default::default())),
468 )?
469 } else if let Some(mut task) = state_root_handle {
470 builder.evm_mut().db_mut().set_state_hook(None);
472
473 match task.state_root() {
477 Ok(outcome) => {
478 debug!(target: "payload_builder", id=%payload_id, state_root=?outcome.state_root, job = task.name(), "received state root from state-root job");
479 builder.finish(
480 state_provider.as_ref(),
481 Some((outcome.state_root, outcome.trie_updates)),
482 )?
483 }
484 Err(err) => {
485 warn!(target: "payload_builder", id=%payload_id, %err, "state-root job failed, falling back to sync state root");
486 builder.finish(state_provider.as_ref(), None)?
487 }
488 }
489 } else {
490 builder.finish(state_provider.as_ref(), None)?
491 };
492
493 let requests = chain_spec
494 .is_prague_active_at_timestamp(attributes.timestamp)
495 .then_some(execution_result.requests);
496
497 debug!(target: "payload_builder", id=%payload_id, sealed_block_header = ?block.sealed_header(), "sealed built block");
498
499 if is_osaka && block.rlp_length() > MAX_RLP_BLOCK_SIZE {
500 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
501 rlp_length: block.rlp_length(),
502 max_rlp_length: MAX_RLP_BLOCK_SIZE,
503 }));
504 }
505
506 let block_access_list = block_access_list.map(|bal| bal.split().1);
507 let payload = EthBuiltPayload::new(Arc::new(block), total_fees, requests, block_access_list)
508 .with_sidecars(blob_sidecars);
510
511 Ok(BuildOutcome::Better { payload, cached_reads })
512}