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::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::ConsensusError;
22use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
23use reth_evm::{
24 database::StateProviderDatabase,
25 execute::{
26 BlockBuilder, BlockBuilderOutcome, BlockExecutionError, BlockExecutor, BlockValidationError,
27 },
28 ConfigureEvm, EvmEnv, NextBlockEnvAttributes, OnStateHook,
29};
30use reth_evm_ethereum::EthEvmConfig;
31use reth_execution_cache::{CachedStateMetrics, CachedStateMetricsSource, CachedStateProvider};
32use reth_payload_builder::{BlobSidecars, EthBuiltPayload};
33use reth_payload_builder_primitives::PayloadBuilderError;
34use reth_payload_primitives::PayloadAttributes as _;
35use reth_primitives_traits::transaction::error::InvalidTransactionError;
36use reth_storage_api::{EvmStateProvider, StateProvider, StateProviderFactory};
37use reth_transaction_pool::{
38 error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
39 BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
40 ValidPoolTransaction,
41};
42use reth_trie_common::updates::TrieUpdates;
43use std::sync::Arc;
44use tracing::{debug, trace, warn};
45
46mod config;
47pub use config::*;
48
49pub mod validator;
50pub use validator::EthereumExecutionPayloadValidator;
51
52type BestTransactionsIter<Pool> = Box<
53 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
54>;
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
59 client: Client,
61 pool: Pool,
63 evm_config: EvmConfig,
65 builder_config: EthereumBuilderConfig,
67}
68
69impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
70 pub const fn new(
72 client: Client,
73 pool: Pool,
74 evm_config: EvmConfig,
75 builder_config: EthereumBuilderConfig,
76 ) -> Self {
77 Self { client, pool, evm_config, builder_config }
78 }
79}
80
81impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
83where
84 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
85 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
86 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
87{
88 type Attributes = EthPayloadAttributes;
89 type BuiltPayload = EthBuiltPayload;
90
91 fn try_build(
92 &self,
93 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
94 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
95 default_ethereum_payload(
96 self.evm_config.clone(),
97 self.client.clone(),
98 self.pool.clone(),
99 self.builder_config.clone(),
100 args,
101 |attributes| self.pool.best_transactions_with_attributes(attributes),
102 )
103 }
104
105 fn on_missing_payload(
106 &self,
107 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
108 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
109 if self.builder_config.await_payload_on_missing {
110 MissingPayloadBehaviour::AwaitInProgress
111 } else {
112 MissingPayloadBehaviour::RaceEmptyPayload
113 }
114 }
115
116 fn build_empty_payload(
117 &self,
118 config: PayloadConfig<Self::Attributes>,
119 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
120 let args = BuildArguments::new(
121 Default::default(),
122 Default::default(),
123 None,
124 config,
125 Default::default(),
126 None,
127 );
128
129 default_ethereum_payload(
130 self.evm_config.clone(),
131 self.client.clone(),
132 self.pool.clone(),
133 self.builder_config.clone(),
134 args,
135 |_| -> BestTransactionsIter<Pool> { Box::new(std::iter::empty()) },
136 )?
137 .into_payload()
138 .ok_or_else(|| PayloadBuilderError::MissingPayload)
139 }
140}
141
142#[inline]
148pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
149 evm_config: EvmConfig,
150 client: Client,
151 pool: Pool,
152 builder_config: EthereumBuilderConfig,
153 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
154 best_txs: F,
155) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
156where
157 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
158 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
159 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
160 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
161{
162 let BuildArguments {
163 mut cached_reads,
164 execution_cache,
165 mut state_root_handle,
166 config,
167 cancel,
168 best_payload,
169 } = args;
170 let PayloadConfig { parent_header, attributes, payload_id, .. } = config;
171 let skip_state_root = builder_config.skip_state_root;
172
173 let state_provider = client.state_by_block_hash(parent_header.hash())?;
174 let evm_state_provider = (&state_provider).into_evm_state_provider();
175 let cached_state_provider = execution_cache.map(|execution_cache| {
176 CachedStateProvider::new(
177 &evm_state_provider,
178 execution_cache.cache().clone(),
179 Some(CachedStateMetrics::zeroed(CachedStateMetricsSource::Builder)),
182 )
183 });
184 let state = StateProviderDatabase::new(
185 cached_state_provider
186 .as_ref()
187 .map(|provider| provider as &dyn EvmStateProvider)
188 .unwrap_or(&evm_state_provider),
189 );
190 let chain_spec = client.chain_spec();
191 let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(attributes.timestamp());
192 let gas_limit = builder_config
193 .gas_limit_with_target(parent_header.gas_limit, attributes.target_gas_limit());
194 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp());
195
196 debug!(
197 target: "payload_builder",
198 id = %payload_id,
199 parent_header = ?parent_header.hash(),
200 parent_number = parent_header.number,
201 "building payload"
202 );
203
204 let cached_db = cached_reads.as_db_mut(state);
205 let evm_config = evm_config.with_jit_support();
206 let stream_state_updates = state_root_handle.is_some() && !skip_state_root;
207 let next_block_env_attributes = NextBlockEnvAttributes {
208 timestamp: attributes.timestamp(),
209 suggested_fee_recipient: attributes.suggested_fee_recipient,
210 prev_randao: attributes.prev_randao,
211 gas_limit,
212 parent_beacon_block_root: attributes.parent_beacon_block_root(),
213 withdrawals: attributes.withdrawals.clone().map(Into::into),
214 extra_data: builder_config.extra_data.clone(),
215 slot_number: attributes.slot_number(),
216 };
217 let mut builder = evm_config
218 .builder_for_next_block(cached_db, &parent_header, next_block_env_attributes)
219 .map_err(PayloadBuilderError::other)?;
220 if is_amsterdam {
221 builder.executor_mut().enable_block_access_list_builder();
222 }
223 let base_fee = builder.evm_env().block_base_fee();
224 let blob_fee = blob_params.as_ref().map(|_| builder.evm_env().block_blob_base_fee());
225 let separate_block_gas = builder.evm_env().uses_separate_block_gas();
226 let regular_gas_limit_cap = builder.evm_env().regular_gas_limit_cap();
227
228 let use_state_root_task =
230 if let Some(task) = state_root_handle.as_mut().filter(|_| stream_state_updates) {
231 let mut hook = task.take_state_hook();
232 builder.set_state_hook(move |state| hook.on_state(state))
233 } else {
234 false
235 };
236
237 builder.apply_pre_execution_changes().map_err(|err| {
238 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
239 PayloadBuilderError::Internal(err.into())
240 })?;
241
242 let mut best_txs = best_txs(BestTransactionsAttributes::new(base_fee, blob_fee));
243 let mut total_fees = U256::ZERO;
244 let mut cumulative_tx_gas_used = 0u64;
245 let mut block_regular_gas_used = 0u64;
246 let mut block_state_gas_used = 0u64;
247 let mut blob_sidecars = BlobSidecars::Empty;
250 let mut block_blob_count = 0;
251 let mut block_transactions_rlp_length = 0;
252 let protocol_max_blob_count =
253 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_default();
254 let max_blob_count = builder_config
257 .max_blobs_per_block
258 .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
259 .unwrap_or(protocol_max_blob_count);
260 let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp());
261 let withdrawals_rlp_length =
262 attributes.withdrawals.as_ref().map(|withdrawals| withdrawals.length()).unwrap_or(0);
263
264 while let Some(pool_tx) = best_txs.next() {
265 if cancel.is_cancelled() {
267 return Ok(BuildOutcome::Cancelled)
268 }
269
270 let exceeds_gas_limit = if separate_block_gas {
272 let regular_available = gas_limit.saturating_sub(block_regular_gas_used);
273 let state_available = gas_limit.saturating_sub(block_state_gas_used);
274 let regular_limit = pool_tx.gas_limit().min(regular_gas_limit_cap);
275 if regular_limit > regular_available {
276 Some((regular_limit, regular_available))
277 } else if pool_tx.gas_limit() > state_available {
278 Some((pool_tx.gas_limit(), state_available))
279 } else {
280 None
281 }
282 } else {
283 let available = gas_limit.saturating_sub(cumulative_tx_gas_used);
284 (pool_tx.gas_limit() > available).then_some((pool_tx.gas_limit(), available))
285 };
286 if let Some((transaction_gas_limit, block_available_gas)) = exceeds_gas_limit {
287 best_txs.mark_invalid(
291 &pool_tx,
292 InvalidPoolTransactionError::ExceedsGasLimit(
293 transaction_gas_limit,
294 block_available_gas,
295 ),
296 );
297 continue
298 }
299
300 let tx = pool_tx.to_consensus();
302 let tx_rlp_len = tx.inner().length();
303 let estimated_block_size_with_tx =
304 block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
307 best_txs.mark_invalid(
308 &pool_tx,
309 InvalidPoolTransactionError::OversizedData {
310 size: estimated_block_size_with_tx,
311 limit: MAX_RLP_BLOCK_SIZE,
312 },
313 );
314 continue
315 }
316
317 let mut blob_tx_sidecar = None;
320 let tx_blob_count = tx.blob_count();
321 if let Some(tx_blob_count) = tx_blob_count {
322 if block_blob_count + tx_blob_count > max_blob_count {
323 trace!(
328 target: "payload_builder",
329 tx = ?tx.hash(),
330 ?block_blob_count,
331 "skipping blob transaction because it would exceed the max blob count per block"
332 );
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 let gas_output = match builder.execute_transaction(tx) {
377 Ok(gas_output) => gas_output,
378 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
379 error, ..
380 })) => {
381 if error.is_nonce_too_low() {
382 trace!(target: "payload_builder", %error, ?tx_hash, "skipping nonce too low transaction");
384 } else {
385 trace!(target: "payload_builder", %error, ?tx_hash, "skipping invalid transaction and its descendants");
388 best_txs.mark_invalid(
389 &pool_tx,
390 InvalidPoolTransactionError::Consensus(
391 InvalidTransactionError::TxTypeNotSupported,
392 ),
393 );
394 }
395 continue
396 }
397 Err(BlockExecutionError::Validation(
400 BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
401 transaction_gas_limit,
402 block_available_gas,
403 },
404 )) => {
405 trace!(target: "payload_builder", %transaction_gas_limit, %block_available_gas, ?tx_hash, "skipping transaction exceeding block gas limit");
406 best_txs.mark_invalid(
407 &pool_tx,
408 InvalidPoolTransactionError::ExceedsGasLimit(
409 transaction_gas_limit,
410 block_available_gas,
411 ),
412 );
413 continue
414 }
415 Err(err) => return Err(PayloadBuilderError::evm(err)),
417 };
418
419 if let Some(blob_count) = tx_blob_count {
421 block_blob_count += blob_count;
422
423 if block_blob_count == max_blob_count {
425 best_txs.skip_blobs();
426 }
427 }
428
429 block_transactions_rlp_length += tx_rlp_len;
430
431 let gas_used = gas_output.tx_gas_used();
433 let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
434 total_fees += U256::from(miner_fee) * U256::from(gas_used);
435 cumulative_tx_gas_used += gas_used;
436 block_regular_gas_used += gas_output.regular_gas_used();
437 block_state_gas_used += gas_output.state_gas_used();
438
439 if let Some(sidecar) = blob_tx_sidecar {
441 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
442 }
443 }
444
445 if !is_better_payload(best_payload.as_ref(), total_fees) {
447 drop(builder);
449 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
451 }
452
453 let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if skip_state_root
454 {
455 debug!(
456 target: "payload_builder",
457 id = %payload_id,
458 state_root = ?parent_header.state_root,
459 "skipping payload state-root computation"
460 );
461 builder.finish(
462 state_provider.as_ref(),
463 Some((parent_header.state_root, TrieUpdates::default())),
464 )?
465 } else if use_state_root_task {
466 let mut task = state_root_handle.expect("state-root task exists if hook was installed");
467 builder.finish_with_state_root(state_provider.as_ref(), |_| match task.state_root() {
471 Ok(outcome) => {
472 debug!(
473 target: "payload_builder",
474 id = %payload_id,
475 state_root = ?outcome.state_root,
476 job = task.name(),
477 "received state root from state-root job"
478 );
479 Ok(Some((outcome.state_root, Arc::unwrap_or_clone(outcome.trie_updates))))
480 }
481 Err(err) => {
482 warn!(target: "payload_builder", id = %payload_id, %err, "state-root job failed, falling back to sync state root");
483 Ok(None)
484 }
485 })?
486 } else {
487 builder.finish(state_provider.as_ref(), None)?
488 };
489
490 let requests = chain_spec
491 .is_prague_active_at_timestamp(attributes.timestamp())
492 .then_some(execution_result.requests);
493
494 debug!(
495 target: "payload_builder",
496 id = %payload_id,
497 sealed_block_header = ?block.sealed_header(),
498 "sealed built block"
499 );
500
501 if is_osaka && block.rlp_length() > MAX_RLP_BLOCK_SIZE {
502 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
503 rlp_length: block.rlp_length(),
504 max_rlp_length: MAX_RLP_BLOCK_SIZE,
505 }));
506 }
507
508 let block_access_list = block_access_list.map(|bal| bal.split().1);
509 let payload = EthBuiltPayload::new(Arc::new(block), total_fees, requests, block_access_list)
510 .with_sidecars(blob_sidecars);
512
513 Ok(BuildOutcome::Better { payload, cached_reads })
514}