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#![allow(clippy::useless_let_if_seq)]
11
12use alloy_consensus::Transaction;
13use alloy_primitives::U256;
14use alloy_rlp::Encodable;
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},
25 ConfigureEvm, Evm, NextBlockEnvAttributes,
26};
27use reth_evm_ethereum::EthEvmConfig;
28use reth_payload_builder::{BlobSidecars, EthBuiltPayload, EthPayloadBuilderAttributes};
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_primitives_traits::transaction::error::InvalidTransactionError;
32use reth_revm::{database::StateProviderDatabase, db::State};
33use reth_storage_api::StateProviderFactory;
34use reth_transaction_pool::{
35 error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
36 BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
37 ValidPoolTransaction,
38};
39use revm::context_interface::Block as _;
40use std::sync::Arc;
41use tracing::{debug, trace, warn};
42
43mod config;
44pub use config::*;
45
46pub mod validator;
47pub use validator::EthereumExecutionPayloadValidator;
48
49type BestTransactionsIter<Pool> = Box<
50 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
51>;
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
56 client: Client,
58 pool: Pool,
60 evm_config: EvmConfig,
62 builder_config: EthereumBuilderConfig,
64}
65
66impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
67 pub const fn new(
69 client: Client,
70 pool: Pool,
71 evm_config: EvmConfig,
72 builder_config: EthereumBuilderConfig,
73 ) -> Self {
74 Self { client, pool, evm_config, builder_config }
75 }
76}
77
78impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
80where
81 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
82 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
83 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
84{
85 type Attributes = EthPayloadBuilderAttributes;
86 type BuiltPayload = EthBuiltPayload;
87
88 fn try_build(
89 &self,
90 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
91 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
92 default_ethereum_payload(
93 self.evm_config.clone(),
94 self.client.clone(),
95 self.pool.clone(),
96 self.builder_config.clone(),
97 args,
98 |attributes| self.pool.best_transactions_with_attributes(attributes),
99 )
100 }
101
102 fn on_missing_payload(
103 &self,
104 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
105 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
106 if self.builder_config.await_payload_on_missing {
107 MissingPayloadBehaviour::AwaitInProgress
108 } else {
109 MissingPayloadBehaviour::RaceEmptyPayload
110 }
111 }
112
113 fn build_empty_payload(
114 &self,
115 config: PayloadConfig<Self::Attributes>,
116 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
117 let args = BuildArguments::new(Default::default(), config, Default::default(), None);
118
119 default_ethereum_payload(
120 self.evm_config.clone(),
121 self.client.clone(),
122 self.pool.clone(),
123 self.builder_config.clone(),
124 args,
125 |attributes| self.pool.best_transactions_with_attributes(attributes),
126 )?
127 .into_payload()
128 .ok_or_else(|| PayloadBuilderError::MissingPayload)
129 }
130}
131
132#[inline]
138pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
139 evm_config: EvmConfig,
140 client: Client,
141 pool: Pool,
142 builder_config: EthereumBuilderConfig,
143 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
144 best_txs: F,
145) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
146where
147 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
148 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
149 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
150 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
151{
152 let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
153 let PayloadConfig { parent_header, attributes } = config;
154
155 let state_provider = client.state_by_block_hash(parent_header.hash())?;
156 let state = StateProviderDatabase::new(state_provider.as_ref());
157 let mut db =
158 State::builder().with_database_ref(cached_reads.as_db(state)).with_bundle_update().build();
159
160 let mut builder = evm_config
161 .builder_for_next_block(
162 &mut db,
163 &parent_header,
164 NextBlockEnvAttributes {
165 timestamp: attributes.timestamp(),
166 suggested_fee_recipient: attributes.suggested_fee_recipient(),
167 prev_randao: attributes.prev_randao(),
168 gas_limit: builder_config.gas_limit(parent_header.gas_limit),
169 parent_beacon_block_root: attributes.parent_beacon_block_root(),
170 withdrawals: Some(attributes.withdrawals().clone()),
171 extra_data: builder_config.extra_data,
172 },
173 )
174 .map_err(PayloadBuilderError::other)?;
175
176 let chain_spec = client.chain_spec();
177
178 debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
179 let mut cumulative_gas_used = 0;
180 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
181 let base_fee = builder.evm_mut().block().basefee();
182
183 let mut best_txs = best_txs(BestTransactionsAttributes::new(
184 base_fee,
185 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
186 ));
187 let mut total_fees = U256::ZERO;
188
189 builder.apply_pre_execution_changes().map_err(|err| {
190 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
191 PayloadBuilderError::Internal(err.into())
192 })?;
193
194 let mut blob_sidecars = BlobSidecars::Empty;
197
198 let mut block_blob_count = 0;
199 let mut block_transactions_rlp_length = 0;
200
201 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
202 let protocol_max_blob_count =
203 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_else(Default::default);
204
205 let max_blob_count = builder_config
208 .max_blobs_per_block
209 .map(|user_limit| std::cmp::min(user_limit, protocol_max_blob_count).max(1))
210 .unwrap_or(protocol_max_blob_count);
211
212 let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
213
214 while let Some(pool_tx) = best_txs.next() {
215 if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
217 best_txs.mark_invalid(
221 &pool_tx,
222 &InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
223 );
224 continue
225 }
226
227 if cancel.is_cancelled() {
229 return Ok(BuildOutcome::Cancelled)
230 }
231
232 let tx = pool_tx.to_consensus();
234
235 let estimated_block_size_with_tx = block_transactions_rlp_length +
236 tx.inner().length() +
237 attributes.withdrawals().length() +
238 1024; if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
241 best_txs.mark_invalid(
242 &pool_tx,
243 &InvalidPoolTransactionError::OversizedData {
244 size: estimated_block_size_with_tx,
245 limit: MAX_RLP_BLOCK_SIZE,
246 },
247 );
248 continue;
249 }
250
251 let mut blob_tx_sidecar = None;
254 if let Some(blob_tx) = tx.as_eip4844() {
255 let tx_blob_count = blob_tx.tx().blob_versioned_hashes.len() as u64;
256
257 if block_blob_count + tx_blob_count > max_blob_count {
258 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
263 best_txs.mark_invalid(
264 &pool_tx,
265 &InvalidPoolTransactionError::Eip4844(
266 Eip4844PoolTransactionError::TooManyEip4844Blobs {
267 have: block_blob_count + tx_blob_count,
268 permitted: max_blob_count,
269 },
270 ),
271 );
272 continue
273 }
274
275 let blob_sidecar_result = 'sidecar: {
276 let Some(sidecar) =
277 pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
278 else {
279 break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
280 };
281
282 if is_osaka {
283 if sidecar.is_eip7594() {
284 Ok(sidecar)
285 } else {
286 Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
287 }
288 } else if sidecar.is_eip4844() {
289 Ok(sidecar)
290 } else {
291 Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
292 }
293 };
294
295 blob_tx_sidecar = match blob_sidecar_result {
296 Ok(sidecar) => Some(sidecar),
297 Err(error) => {
298 best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
299 continue
300 }
301 };
302 }
303
304 let gas_used = match builder.execute_transaction(tx.clone()) {
305 Ok(gas_used) => gas_used,
306 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
307 error, ..
308 })) => {
309 if error.is_nonce_too_low() {
310 trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
312 } else {
313 trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
316 best_txs.mark_invalid(
317 &pool_tx,
318 &InvalidPoolTransactionError::Consensus(
319 InvalidTransactionError::TxTypeNotSupported,
320 ),
321 );
322 }
323 continue
324 }
325 Err(err) => return Err(PayloadBuilderError::evm(err)),
327 };
328
329 if let Some(blob_tx) = tx.as_eip4844() {
331 block_blob_count += blob_tx.tx().blob_versioned_hashes.len() as u64;
332
333 if block_blob_count == max_blob_count {
335 best_txs.skip_blobs();
336 }
337 }
338
339 block_transactions_rlp_length += tx.inner().length();
340
341 let miner_fee =
343 tx.effective_tip_per_gas(base_fee).expect("fee is always valid; execution succeeded");
344 total_fees += U256::from(miner_fee) * U256::from(gas_used);
345 cumulative_gas_used += gas_used;
346
347 if let Some(sidecar) = blob_tx_sidecar {
349 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
350 }
351 }
352
353 if !is_better_payload(best_payload.as_ref(), total_fees) {
355 drop(builder);
357 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
359 }
360
361 let BlockBuilderOutcome { execution_result, block, .. } =
362 builder.finish(state_provider.as_ref())?;
363
364 let requests = chain_spec
365 .is_prague_active_at_timestamp(attributes.timestamp)
366 .then_some(execution_result.requests);
367
368 let sealed_block = Arc::new(block.sealed_block().clone());
369 debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
370
371 if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE {
372 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
373 rlp_length: sealed_block.rlp_length(),
374 max_rlp_length: MAX_RLP_BLOCK_SIZE,
375 }));
376 }
377
378 let payload = EthBuiltPayload::new(attributes.id, sealed_block, total_fees, requests)
379 .with_sidecars(blob_sidecars);
381
382 Ok(BuildOutcome::Better { payload, cached_reads })
383}