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/SeismicSystems/seismic-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
12use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH};
13use alloy_eips::{
14 eip4844::MAX_DATA_GAS_PER_BLOCK, eip7002::WITHDRAWAL_REQUEST_TYPE,
15 eip7251::CONSOLIDATION_REQUEST_TYPE, eip7685::Requests, merge::BEACON_NONCE,
16};
17use alloy_primitives::U256;
18use reth_basic_payload_builder::{
19 commit_withdrawals, is_better_payload, BuildArguments, BuildOutcome, PayloadBuilder,
20 PayloadConfig,
21};
22use reth_chain_state::ExecutedBlock;
23use reth_chainspec::{ChainSpec, ChainSpecProvider};
24use reth_errors::RethError;
25use reth_evm::{system_calls::SystemCaller, ConfigureEvm, NextBlockEnvAttributes};
26use reth_evm_ethereum::{eip6110::parse_deposits_from_receipts, EthEvmConfig};
27use reth_execution_types::ExecutionOutcome;
28use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes};
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_primitives::{
32 proofs::{self},
33 Block, BlockBody, BlockExt, EthereumHardforks, InvalidTransactionError, Receipt,
34 TransactionSigned,
35};
36use reth_revm::database::StateProviderDatabase;
37use reth_transaction_pool::{
38 error::InvalidPoolTransactionError, noop::NoopTransactionPool, BestTransactions,
39 BestTransactionsAttributes, PoolTransaction, TransactionPool, ValidPoolTransaction,
40};
41use revm::{
42 db::{states::bundle_state::BundleRetention, State},
43 primitives::{
44 calc_excess_blob_gas, BlockEnv, CfgEnvWithHandlerCfg, EVMError, EnvWithHandlerCfg,
45 InvalidTransaction, ResultAndState, TxEnv,
46 },
47 DatabaseCommit,
48};
49use std::sync::Arc;
50use tracing::{debug, trace, warn};
51
52mod config;
53pub use config::*;
54use reth_storage_api::StateProviderFactory;
55
56type BestTransactionsIter<Pool> = Box<
57 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
58>;
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct EthereumPayloadBuilder<EvmConfig = EthEvmConfig> {
63 evm_config: EvmConfig,
65 builder_config: EthereumBuilderConfig,
67}
68
69impl<EvmConfig> EthereumPayloadBuilder<EvmConfig> {
70 pub const fn new(evm_config: EvmConfig, builder_config: EthereumBuilderConfig) -> Self {
72 Self { evm_config, builder_config }
73 }
74}
75
76impl<EvmConfig> EthereumPayloadBuilder<EvmConfig>
77where
78 EvmConfig: ConfigureEvm<Header = Header>,
79{
80 fn cfg_and_block_env(
83 &self,
84 config: &PayloadConfig<EthPayloadBuilderAttributes>,
85 parent: &Header,
86 ) -> Result<(CfgEnvWithHandlerCfg, BlockEnv), EvmConfig::Error> {
87 let next_attributes = NextBlockEnvAttributes {
88 timestamp: config.attributes.timestamp(),
89 suggested_fee_recipient: config.attributes.suggested_fee_recipient(),
90 prev_randao: config.attributes.prev_randao(),
91 };
92 self.evm_config.next_cfg_and_block_env(parent, next_attributes)
93 }
94}
95
96impl<EvmConfig, Pool, Client> PayloadBuilder<Pool, Client> for EthereumPayloadBuilder<EvmConfig>
98where
99 EvmConfig: ConfigureEvm<Header = Header, Transaction = TransactionSigned>,
100 Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
101 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
102{
103 type Attributes = EthPayloadBuilderAttributes;
104 type BuiltPayload = EthBuiltPayload;
105
106 fn try_build(
107 &self,
108 args: BuildArguments<Pool, Client, EthPayloadBuilderAttributes, EthBuiltPayload>,
109 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
110 let (cfg_env, block_env) = self
111 .cfg_and_block_env(&args.config, &args.config.parent_header)
112 .map_err(PayloadBuilderError::other)?;
113
114 let pool = args.pool.clone();
115 default_ethereum_payload(
116 self.evm_config.clone(),
117 self.builder_config.clone(),
118 args,
119 cfg_env,
120 block_env,
121 |attributes| pool.best_transactions_with_attributes(attributes),
122 )
123 }
124
125 fn build_empty_payload(
126 &self,
127 client: &Client,
128 config: PayloadConfig<Self::Attributes>,
129 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
130 let args = BuildArguments::new(
131 client,
132 NoopTransactionPool::default(),
134 Default::default(),
135 config,
136 Default::default(),
137 None,
138 );
139
140 let (cfg_env, block_env) = self
141 .cfg_and_block_env(&args.config, &args.config.parent_header)
142 .map_err(PayloadBuilderError::other)?;
143
144 let pool = args.pool.clone();
145
146 default_ethereum_payload(
147 self.evm_config.clone(),
148 self.builder_config.clone(),
149 args,
150 cfg_env,
151 block_env,
152 |attributes| pool.best_transactions_with_attributes(attributes),
153 )?
154 .into_payload()
155 .ok_or_else(|| PayloadBuilderError::MissingPayload)
156 }
157}
158
159#[inline]
165pub fn default_ethereum_payload<EvmConfig, Pool, Client, F>(
166 evm_config: EvmConfig,
167 builder_config: EthereumBuilderConfig,
168 args: BuildArguments<Pool, Client, EthPayloadBuilderAttributes, EthBuiltPayload>,
169 initialized_cfg: CfgEnvWithHandlerCfg,
170 initialized_block_env: BlockEnv,
171 best_txs: F,
172) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
173where
174 EvmConfig: ConfigureEvm<Header = Header, Transaction = TransactionSigned>,
175 Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
176 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
177 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
178{
179 let BuildArguments { client, pool, mut cached_reads, config, cancel, best_payload } = args;
180
181 let chain_spec = client.chain_spec();
182 let state_provider = client.state_by_block_hash(config.parent_header.hash())?;
183 let state = StateProviderDatabase::new(state_provider);
184 let mut db =
185 State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
186 let PayloadConfig { parent_header, attributes } = config;
187
188 debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
189 let mut cumulative_gas_used = 0;
190 let mut sum_blob_gas_used = 0;
191 let block_gas_limit: u64 = initialized_block_env.gas_limit.to::<u64>();
192 let base_fee = initialized_block_env.basefee.to::<u64>();
193
194 let mut executed_txs = Vec::new();
195 let mut executed_senders = Vec::new();
196
197 let mut best_txs = best_txs(BestTransactionsAttributes::new(
198 base_fee,
199 initialized_block_env.get_blob_gasprice().map(|gasprice| gasprice as u64),
200 ));
201 let mut total_fees = U256::ZERO;
202
203 let block_number = initialized_block_env.number.to::<u64>();
204
205 let mut system_caller = SystemCaller::new(evm_config.clone(), chain_spec.clone());
206
207 system_caller
209 .pre_block_beacon_root_contract_call(
210 &mut db,
211 &initialized_cfg,
212 &initialized_block_env,
213 attributes.parent_beacon_block_root,
214 )
215 .map_err(|err| {
216 warn!(target: "payload_builder",
217 parent_hash=%parent_header.hash(),
218 %err,
219 "failed to apply beacon root contract call for payload"
220 );
221 PayloadBuilderError::Internal(err.into())
222 })?;
223
224 system_caller.pre_block_blockhashes_contract_call(
226 &mut db,
227 &initialized_cfg,
228 &initialized_block_env,
229 parent_header.hash(),
230 )
231 .map_err(|err| {
232 warn!(target: "payload_builder", parent_hash=%parent_header.hash(), %err, "failed to update parent header blockhashes for payload");
233 PayloadBuilderError::Internal(err.into())
234 })?;
235
236 let env = EnvWithHandlerCfg::new_with_cfg_env(
237 initialized_cfg.clone(),
238 initialized_block_env.clone(),
239 TxEnv::default(),
240 );
241 let mut evm = evm_config.evm_with_env(&mut db, env);
242
243 let mut receipts = Vec::new();
244 while let Some(pool_tx) = best_txs.next() {
245 if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
247 best_txs.mark_invalid(
251 &pool_tx,
252 InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
253 );
254 continue
255 }
256
257 if cancel.is_cancelled() {
259 return Ok(BuildOutcome::Cancelled)
260 }
261
262 let tx = pool_tx.to_consensus();
264
265 if let Some(blob_tx) = tx.transaction.as_eip4844() {
268 let tx_blob_gas = blob_tx.blob_gas();
269 if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK {
270 trace!(target: "payload_builder", tx=?tx.hash, ?sum_blob_gas_used, ?tx_blob_gas, "skipping blob transaction because it would exceed the max data gas per block");
275 best_txs.mark_invalid(
276 &pool_tx,
277 InvalidPoolTransactionError::ExceedsGasLimit(
278 tx_blob_gas,
279 MAX_DATA_GAS_PER_BLOCK,
280 ),
281 );
282 continue
283 }
284 }
285
286 *evm.tx_mut() = evm_config.tx_env(tx.as_signed(), tx.signer()).map_err(|err| {
288 warn!(target: "payload_builder", %err, ?tx, "failed to configure tx environment for payload");
289 PayloadBuilderError::EvmExecutionError(err.map_db_err(|err|err.into()))
290 })?;
291
292 let ResultAndState { result, state } = match evm.transact() {
293 Ok(res) => res,
294 Err(err) => {
295 match err {
296 EVMError::Transaction(err) => {
297 if matches!(err, InvalidTransaction::NonceTooLow { .. }) {
298 trace!(target: "payload_builder", %err, ?tx, "skipping nonce too low transaction");
300 } else {
301 trace!(target: "payload_builder", %err, ?tx, "skipping invalid transaction and its descendants");
304 best_txs.mark_invalid(
305 &pool_tx,
306 InvalidPoolTransactionError::Consensus(
307 InvalidTransactionError::TxTypeNotSupported,
308 ),
309 );
310 }
311
312 continue
313 }
314 err => {
315 return Err(PayloadBuilderError::EvmExecutionError(err))
317 }
318 }
319 }
320 };
321
322 evm.db_mut().commit(state);
324
325 if let Some(blob_tx) = tx.transaction.as_eip4844() {
327 let tx_blob_gas = blob_tx.blob_gas();
328 sum_blob_gas_used += tx_blob_gas;
329
330 if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK {
332 best_txs.skip_blobs();
333 }
334 }
335
336 let gas_used = result.gas_used();
337
338 cumulative_gas_used += gas_used;
340
341 #[allow(clippy::needless_update)] receipts.push(Some(Receipt {
344 tx_type: tx.tx_type(),
345 success: result.is_success(),
346 cumulative_gas_used,
347 logs: result.into_logs().into_iter().map(Into::into).collect(),
348 ..Default::default()
349 }));
350
351 let miner_fee = tx
353 .effective_tip_per_gas(Some(base_fee))
354 .expect("fee is always valid; execution succeeded");
355 total_fees += U256::from(miner_fee) * U256::from(gas_used);
356
357 executed_senders.push(tx.signer());
359 executed_txs.push(tx.into_signed());
360 }
361
362 drop(evm);
364
365 if !is_better_payload(best_payload.as_ref(), total_fees) {
367 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
369 }
370
371 let requests = if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) {
373 let deposit_requests = parse_deposits_from_receipts(&chain_spec, receipts.iter().flatten())
374 .map_err(|err| PayloadBuilderError::Internal(RethError::Execution(err.into())))?;
375 let withdrawal_requests = system_caller
376 .post_block_withdrawal_requests_contract_call(
377 &mut db,
378 &initialized_cfg,
379 &initialized_block_env,
380 )
381 .map_err(|err| PayloadBuilderError::Internal(err.into()))?;
382 let consolidation_requests = system_caller
383 .post_block_consolidation_requests_contract_call(
384 &mut db,
385 &initialized_cfg,
386 &initialized_block_env,
387 )
388 .map_err(|err| PayloadBuilderError::Internal(err.into()))?;
389
390 let mut requests = Requests::default();
391
392 if !deposit_requests.is_empty() {
393 requests.push_request(core::iter::once(0).chain(deposit_requests).collect());
394 }
395
396 if !withdrawal_requests.is_empty() {
397 requests.push_request(
398 core::iter::once(WITHDRAWAL_REQUEST_TYPE).chain(withdrawal_requests).collect(),
399 );
400 }
401
402 if !consolidation_requests.is_empty() {
403 requests.push_request(
404 core::iter::once(CONSOLIDATION_REQUEST_TYPE)
405 .chain(consolidation_requests)
406 .collect(),
407 );
408 }
409
410 Some(requests)
411 } else {
412 None
413 };
414
415 let withdrawals_root =
416 commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, &attributes.withdrawals)?;
417
418 db.merge_transitions(BundleRetention::Reverts);
421
422 let requests_hash = requests.as_ref().map(|requests| requests.requests_hash());
423 let execution_outcome = ExecutionOutcome::new(
424 db.take_bundle(),
425 vec![receipts].into(),
426 block_number,
427 vec![requests.clone().unwrap_or_default()],
428 );
429 let receipts_root =
430 execution_outcome.ethereum_receipts_root(block_number).expect("Number is in range");
431 let logs_bloom = execution_outcome.block_logs_bloom(block_number).expect("Number is in range");
432
433 let hashed_state = db.database.db.hashed_post_state(execution_outcome.state());
435 let (state_root, trie_output) = {
436 db.database.inner().state_root_with_updates(hashed_state.clone()).inspect_err(|err| {
437 warn!(target: "payload_builder",
438 parent_hash=%parent_header.hash(),
439 %err,
440 "failed to calculate state root for payload"
441 );
442 })?
443 };
444
445 let transactions_root = proofs::calculate_transaction_root(&executed_txs);
447
448 let mut blob_sidecars = Vec::new();
450 let mut excess_blob_gas = None;
451 let mut blob_gas_used = None;
452
453 if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp) {
455 blob_sidecars = pool
457 .get_all_blobs_exact(
458 executed_txs.iter().filter(|tx| tx.is_eip4844()).map(|tx| tx.hash()).collect(),
459 )
460 .map_err(PayloadBuilderError::other)?;
461
462 excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_header.timestamp) {
463 let parent_excess_blob_gas = parent_header.excess_blob_gas.unwrap_or_default();
464 let parent_blob_gas_used = parent_header.blob_gas_used.unwrap_or_default();
465 Some(calc_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used))
466 } else {
467 Some(calc_excess_blob_gas(0, 0))
470 };
471
472 blob_gas_used = Some(sum_blob_gas_used);
473 }
474
475 let header = Header {
476 parent_hash: parent_header.hash(),
477 ommers_hash: EMPTY_OMMER_ROOT_HASH,
478 beneficiary: initialized_block_env.coinbase,
479 state_root,
480 transactions_root,
481 receipts_root,
482 withdrawals_root,
483 logs_bloom,
484 timestamp: attributes.timestamp,
485 mix_hash: attributes.prev_randao,
486 nonce: BEACON_NONCE.into(),
487 base_fee_per_gas: Some(base_fee),
488 number: parent_header.number + 1,
489 gas_limit: block_gas_limit,
490 difficulty: U256::ZERO,
491 gas_used: cumulative_gas_used,
492 extra_data: builder_config.extra_data,
493 parent_beacon_block_root: attributes.parent_beacon_block_root,
494 blob_gas_used: blob_gas_used.map(Into::into),
495 excess_blob_gas: excess_blob_gas.map(Into::into),
496 requests_hash,
497 target_blobs_per_block: None,
498 };
499
500 let withdrawals = chain_spec
501 .is_shanghai_active_at_timestamp(attributes.timestamp)
502 .then(|| attributes.withdrawals.clone());
503
504 let block = Block {
506 header,
507 body: BlockBody { transactions: executed_txs, ommers: vec![], withdrawals },
508 };
509
510 let sealed_block = Arc::new(block.seal_slow());
511 debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.header, "sealed built block");
512
513 let executed = ExecutedBlock {
515 block: sealed_block.clone(),
516 senders: Arc::new(executed_senders),
517 execution_output: Arc::new(execution_outcome),
518 hashed_state: Arc::new(hashed_state),
519 trie: Arc::new(trie_output),
520 };
521
522 let mut payload =
523 EthBuiltPayload::new(attributes.id, sealed_block, total_fees, Some(executed), requests);
524
525 payload.extend_sidecars(blob_sidecars.into_iter().map(Arc::unwrap_or_clone));
527
528 Ok(BuildOutcome::Better { payload, cached_reads })
529}