reth_ethereum_payload_builder/
lib.rs1#![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::{BlockExecutionError, BlockValidationError, ConsensusError};
22use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
23use reth_evm::{
24 execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
25 ConfigureEvm, Evm, NextBlockEnvAttributes,
26};
27use reth_evm_ethereum::EthEvmConfig;
28use reth_execution_cache::CachedStateProvider;
29use reth_payload_builder::{BlobSidecars, EthBuiltPayload};
30use reth_payload_builder_primitives::PayloadBuilderError;
31use reth_payload_primitives::PayloadAttributes;
32use reth_primitives_traits::transaction::error::InvalidTransactionError;
33use reth_revm::{database::StateProviderDatabase, db::State};
34use reth_storage_api::StateProviderFactory;
35use reth_transaction_pool::{
36 error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
37 BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
38 ValidPoolTransaction,
39};
40use revm::context_interface::Block as _;
41use std::sync::Arc;
42use tracing::{debug, trace, warn};
43
44mod config;
45pub use config::*;
46
47pub mod validator;
48pub use validator::EthereumExecutionPayloadValidator;
49
50type BestTransactionsIter<Pool> = Box<
51 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
52>;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
57 client: Client,
59 pool: Pool,
61 evm_config: EvmConfig,
63 builder_config: EthereumBuilderConfig,
65}
66
67impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
68 pub const fn new(
70 client: Client,
71 pool: Pool,
72 evm_config: EvmConfig,
73 builder_config: EthereumBuilderConfig,
74 ) -> Self {
75 Self { client, pool, evm_config, builder_config }
76 }
77}
78
79impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
81where
82 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
83 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
84 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
85{
86 type Attributes = EthPayloadAttributes;
87 type BuiltPayload = EthBuiltPayload;
88
89 fn try_build(
90 &self,
91 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
92 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
93 default_ethereum_payload(
94 self.evm_config.clone(),
95 self.client.clone(),
96 self.pool.clone(),
97 self.builder_config.clone(),
98 args,
99 |attributes| self.pool.best_transactions_with_attributes(attributes),
100 )
101 }
102
103 fn on_missing_payload(
104 &self,
105 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
106 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
107 if self.builder_config.await_payload_on_missing {
108 MissingPayloadBehaviour::AwaitInProgress
109 } else {
110 MissingPayloadBehaviour::RaceEmptyPayload
111 }
112 }
113
114 fn build_empty_payload(
115 &self,
116 config: PayloadConfig<Self::Attributes>,
117 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
118 let args = BuildArguments::new(
119 Default::default(),
120 Default::default(),
121 None,
122 config,
123 Default::default(),
124 None,
125 );
126
127 default_ethereum_payload(
128 self.evm_config.clone(),
129 self.client.clone(),
130 self.pool.clone(),
131 self.builder_config.clone(),
132 args,
133 |attributes| self.pool.best_transactions_with_attributes(attributes),
134 )?
135 .into_payload()
136 .ok_or_else(|| PayloadBuilderError::MissingPayload)
137 }
138}
139
140#[inline]
146pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
147 evm_config: EvmConfig,
148 client: Client,
149 pool: Pool,
150 builder_config: EthereumBuilderConfig,
151 args: BuildArguments<EthPayloadAttributes, EthBuiltPayload>,
152 best_txs: F,
153) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
154where
155 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
156 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
157 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
158 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
159{
160 let BuildArguments {
161 mut cached_reads,
162 execution_cache,
163 trie_handle,
164 config,
165 cancel,
166 best_payload,
167 } = args;
168 let PayloadConfig { parent_header, attributes, payload_id } = config;
169
170 let mut state_provider = client.state_by_block_hash(parent_header.hash())?;
171 if let Some(execution_cache) = execution_cache {
172 state_provider = Box::new(CachedStateProvider::new(
173 state_provider,
174 execution_cache.cache().clone(),
175 execution_cache.metrics().clone(),
176 ));
177 }
178 let state = StateProviderDatabase::new(state_provider.as_ref());
179 let mut db =
180 State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
181
182 let mut builder = evm_config
183 .builder_for_next_block(
184 &mut db,
185 &parent_header,
186 NextBlockEnvAttributes {
187 timestamp: attributes.timestamp(),
188 suggested_fee_recipient: attributes.suggested_fee_recipient,
189 prev_randao: attributes.prev_randao,
190 gas_limit: builder_config.gas_limit(parent_header.gas_limit),
191 parent_beacon_block_root: attributes.parent_beacon_block_root(),
192 withdrawals: attributes.withdrawals.clone().map(Into::into),
193 extra_data: builder_config.extra_data,
194 },
195 )
196 .map_err(PayloadBuilderError::other)?;
197
198 let chain_spec = client.chain_spec();
199
200 debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
201 let mut cumulative_gas_used = 0;
202 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
203 let base_fee = builder.evm_mut().block().basefee();
204
205 let mut best_txs = best_txs(BestTransactionsAttributes::new(
206 base_fee,
207 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
208 ));
209 let mut total_fees = U256::ZERO;
210
211 if let Some(ref handle) = trie_handle {
214 builder.executor_mut().set_state_hook(Some(Box::new(handle.state_hook())));
215 }
216
217 builder.apply_pre_execution_changes().map_err(|err| {
218 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
219 PayloadBuilderError::Internal(err.into())
220 })?;
221
222 let mut blob_sidecars = BlobSidecars::Empty;
225
226 let mut block_blob_count = 0;
227 let mut block_transactions_rlp_length = 0;
228
229 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
230 let protocol_max_blob_count =
231 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_else(Default::default);
232
233 let max_blob_count = builder_config
236 .max_blobs_per_block
237 .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
238 .unwrap_or(protocol_max_blob_count);
239
240 let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
241
242 let withdrawals_rlp_length =
243 attributes.withdrawals.as_ref().map(|withdrawals| withdrawals.length()).unwrap_or(0);
244
245 while let Some(pool_tx) = best_txs.next() {
246 if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
248 best_txs.mark_invalid(
252 &pool_tx,
253 &InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
254 );
255 continue
256 }
257
258 if cancel.is_cancelled() {
260 return Ok(BuildOutcome::Cancelled)
261 }
262
263 let tx = pool_tx.to_consensus();
265
266 let tx_rlp_len = tx.inner().length();
267
268 let estimated_block_size_with_tx =
269 block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
272 best_txs.mark_invalid(
273 &pool_tx,
274 &InvalidPoolTransactionError::OversizedData {
275 size: estimated_block_size_with_tx,
276 limit: MAX_RLP_BLOCK_SIZE,
277 },
278 );
279 continue
280 }
281
282 let mut blob_tx_sidecar = None;
285 let tx_blob_count = tx.blob_count();
286
287 if let Some(tx_blob_count) = tx_blob_count {
288 if block_blob_count + tx_blob_count > max_blob_count {
289 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
294 best_txs.mark_invalid(
295 &pool_tx,
296 &InvalidPoolTransactionError::Eip4844(
297 Eip4844PoolTransactionError::TooManyEip4844Blobs {
298 have: block_blob_count + tx_blob_count,
299 permitted: max_blob_count,
300 },
301 ),
302 );
303 continue
304 }
305
306 let blob_sidecar_result = 'sidecar: {
307 let Some(sidecar) =
308 pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
309 else {
310 break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
311 };
312
313 if is_osaka {
314 if sidecar.is_eip7594() {
315 Ok(sidecar)
316 } else {
317 Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
318 }
319 } else if sidecar.is_eip4844() {
320 Ok(sidecar)
321 } else {
322 Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
323 }
324 };
325
326 blob_tx_sidecar = match blob_sidecar_result {
327 Ok(sidecar) => Some(sidecar),
328 Err(error) => {
329 best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
330 continue
331 }
332 };
333 }
334
335 let miner_fee = tx.effective_tip_per_gas(base_fee);
336 let tx_hash = *tx.tx_hash();
337
338 let gas_used = match builder.execute_transaction(tx) {
339 Ok(gas_used) => gas_used,
340 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
341 error, ..
342 })) => {
343 if error.is_nonce_too_low() {
344 trace!(target: "payload_builder", %error, ?tx_hash, "skipping nonce too low transaction");
346 } else {
347 trace!(target: "payload_builder", %error, ?tx_hash, "skipping invalid transaction and its descendants");
350 best_txs.mark_invalid(
351 &pool_tx,
352 &InvalidPoolTransactionError::Consensus(
353 InvalidTransactionError::TxTypeNotSupported,
354 ),
355 );
356 }
357 continue
358 }
359 Err(err) => return Err(PayloadBuilderError::evm(err)),
361 };
362
363 if let Some(blob_count) = tx_blob_count {
365 block_blob_count += blob_count;
366
367 if block_blob_count == max_blob_count {
369 best_txs.skip_blobs();
370 }
371 }
372
373 block_transactions_rlp_length += tx_rlp_len;
374
375 let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
377 total_fees += U256::from(miner_fee) * U256::from(gas_used);
378 cumulative_gas_used += gas_used;
379
380 if let Some(sidecar) = blob_tx_sidecar {
382 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
383 }
384 }
385
386 if !is_better_payload(best_payload.as_ref(), total_fees) {
388 drop(builder);
390 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
392 }
393
394 let BlockBuilderOutcome { execution_result, block, .. } = if let Some(mut handle) = trie_handle
395 {
396 builder.executor_mut().set_state_hook(None);
399
400 match handle.state_root() {
404 Ok(outcome) => {
405 debug!(target: "payload_builder", id=%payload_id, state_root=?outcome.state_root, "received state root from sparse trie");
406 builder.finish(
407 state_provider.as_ref(),
408 Some((outcome.state_root, Arc::unwrap_or_clone(outcome.trie_updates))),
409 )?
410 }
411 Err(err) => {
412 warn!(target: "payload_builder", id=%payload_id, %err, "sparse trie failed, falling back to sync state root");
413 builder.finish(state_provider.as_ref(), None)?
414 }
415 }
416 } else {
417 builder.finish(state_provider.as_ref(), None)?
418 };
419
420 let requests = chain_spec
421 .is_prague_active_at_timestamp(attributes.timestamp)
422 .then_some(execution_result.requests);
423
424 let sealed_block = Arc::new(block.into_sealed_block());
425 debug!(target: "payload_builder", id=%payload_id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
426
427 if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE {
428 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
429 rlp_length: sealed_block.rlp_length(),
430 max_rlp_length: MAX_RLP_BLOCK_SIZE,
431 }));
432 }
433
434 let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
435 .with_sidecars(blob_sidecars);
437
438 Ok(BuildOutcome::Better { payload, cached_reads })
439}