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/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::Transaction;
13use alloy_primitives::U256;
14use reth_basic_payload_builder::{
15 is_better_payload, BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder,
16 PayloadConfig,
17};
18use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
19use reth_errors::{BlockExecutionError, BlockValidationError};
20use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
21use reth_evm::{
22 execute::{BlockBuilder, BlockBuilderOutcome},
23 ConfigureEvm, Evm, NextBlockEnvAttributes,
24};
25use reth_evm_ethereum::EthEvmConfig;
26use reth_payload_builder::{BlobSidecars, EthBuiltPayload, EthPayloadBuilderAttributes};
27use reth_payload_builder_primitives::PayloadBuilderError;
28use reth_payload_primitives::PayloadBuilderAttributes;
29use reth_primitives_traits::transaction::error::InvalidTransactionError;
30use reth_revm::{database::StateProviderDatabase, db::State};
31use reth_storage_api::StateProviderFactory;
32use reth_transaction_pool::{
33 error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
34 BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
35 ValidPoolTransaction,
36};
37use revm::context_interface::Block as _;
38use std::sync::Arc;
39use tracing::{debug, trace, warn};
40
41mod config;
42pub use config::*;
43
44pub mod validator;
45pub use validator::EthereumExecutionPayloadValidator;
46
47type BestTransactionsIter<Pool> = Box<
48 dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
49>;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct EthereumPayloadBuilder<Pool, Client, EvmConfig = EthEvmConfig> {
54 client: Client,
56 pool: Pool,
58 evm_config: EvmConfig,
60 builder_config: EthereumBuilderConfig,
62}
63
64impl<Pool, Client, EvmConfig> EthereumPayloadBuilder<Pool, Client, EvmConfig> {
65 pub const fn new(
67 client: Client,
68 pool: Pool,
69 evm_config: EvmConfig,
70 builder_config: EthereumBuilderConfig,
71 ) -> Self {
72 Self { client, pool, evm_config, builder_config }
73 }
74}
75
76impl<Pool, Client, EvmConfig> PayloadBuilder for EthereumPayloadBuilder<Pool, Client, EvmConfig>
78where
79 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
80 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks> + Clone,
81 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
82{
83 type Attributes = EthPayloadBuilderAttributes;
84 type BuiltPayload = EthBuiltPayload;
85
86 fn try_build(
87 &self,
88 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
89 ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
90 default_ethereum_payload(
91 self.evm_config.clone(),
92 self.client.clone(),
93 self.pool.clone(),
94 self.builder_config.clone(),
95 args,
96 |attributes| self.pool.best_transactions_with_attributes(attributes),
97 )
98 }
99
100 fn on_missing_payload(
101 &self,
102 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
103 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
104 if self.builder_config.await_payload_on_missing {
105 MissingPayloadBehaviour::AwaitInProgress
106 } else {
107 MissingPayloadBehaviour::RaceEmptyPayload
108 }
109 }
110
111 fn build_empty_payload(
112 &self,
113 config: PayloadConfig<Self::Attributes>,
114 ) -> Result<EthBuiltPayload, PayloadBuilderError> {
115 let args = BuildArguments::new(Default::default(), config, Default::default(), None);
116
117 default_ethereum_payload(
118 self.evm_config.clone(),
119 self.client.clone(),
120 self.pool.clone(),
121 self.builder_config.clone(),
122 args,
123 |attributes| self.pool.best_transactions_with_attributes(attributes),
124 )?
125 .into_payload()
126 .ok_or_else(|| PayloadBuilderError::MissingPayload)
127 }
128}
129
130#[inline]
136pub fn default_ethereum_payload<EvmConfig, Client, Pool, F>(
137 evm_config: EvmConfig,
138 client: Client,
139 pool: Pool,
140 builder_config: EthereumBuilderConfig,
141 args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload>,
142 best_txs: F,
143) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
144where
145 EvmConfig: ConfigureEvm<Primitives = EthPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
146 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthereumHardforks>,
147 Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
148 F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
149{
150 let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
151 let PayloadConfig { parent_header, attributes } = config;
152
153 let state_provider = client.state_by_block_hash(parent_header.hash())?;
154 let state = StateProviderDatabase::new(&state_provider);
155 let mut db =
156 State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
157
158 let mut builder = evm_config
159 .builder_for_next_block(
160 &mut db,
161 &parent_header,
162 NextBlockEnvAttributes {
163 timestamp: attributes.timestamp(),
164 suggested_fee_recipient: attributes.suggested_fee_recipient(),
165 prev_randao: attributes.prev_randao(),
166 gas_limit: builder_config.gas_limit(parent_header.gas_limit),
167 parent_beacon_block_root: attributes.parent_beacon_block_root(),
168 withdrawals: Some(attributes.withdrawals().clone()),
169 },
170 )
171 .map_err(PayloadBuilderError::other)?;
172
173 let chain_spec = client.chain_spec();
174
175 debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
176 let mut cumulative_gas_used = 0;
177 let block_gas_limit: u64 = builder.evm_mut().block().gas_limit;
178 let base_fee = builder.evm_mut().block().basefee;
179
180 let mut best_txs = best_txs(BestTransactionsAttributes::new(
181 base_fee,
182 builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
183 ));
184 let mut total_fees = U256::ZERO;
185
186 builder.apply_pre_execution_changes().map_err(|err| {
187 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
188 PayloadBuilderError::Internal(err.into())
189 })?;
190
191 let mut blob_sidecars = BlobSidecars::Empty;
194
195 let mut block_blob_count = 0;
196
197 let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp);
198 let max_blob_count =
199 blob_params.as_ref().map(|params| params.max_blob_count).unwrap_or_default();
200
201 while let Some(pool_tx) = best_txs.next() {
202 if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
204 best_txs.mark_invalid(
208 &pool_tx,
209 InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
210 );
211 continue
212 }
213
214 if cancel.is_cancelled() {
216 return Ok(BuildOutcome::Cancelled)
217 }
218
219 let tx = pool_tx.to_consensus();
221
222 let mut blob_tx_sidecar = None;
225 if let Some(blob_tx) = tx.as_eip4844() {
226 let tx_blob_count = blob_tx.tx().blob_versioned_hashes.len() as u64;
227
228 if block_blob_count + tx_blob_count > max_blob_count {
229 trace!(target: "payload_builder", tx=?tx.hash(), ?block_blob_count, "skipping blob transaction because it would exceed the max blob count per block");
234 best_txs.mark_invalid(
235 &pool_tx,
236 InvalidPoolTransactionError::Eip4844(
237 Eip4844PoolTransactionError::TooManyEip4844Blobs {
238 have: block_blob_count + tx_blob_count,
239 permitted: max_blob_count,
240 },
241 ),
242 );
243 continue
244 }
245
246 let blob_sidecar_result = 'sidecar: {
247 let Some(sidecar) =
248 pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
249 else {
250 break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
251 };
252
253 if chain_spec.is_osaka_active_at_timestamp(attributes.timestamp) {
254 if sidecar.is_eip7594() {
255 Ok(sidecar)
256 } else {
257 Err(Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka)
258 }
259 } else if sidecar.is_eip4844() {
260 Ok(sidecar)
261 } else {
262 Err(Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka)
263 }
264 };
265
266 blob_tx_sidecar = match blob_sidecar_result {
267 Ok(sidecar) => Some(sidecar),
268 Err(error) => {
269 best_txs.mark_invalid(&pool_tx, InvalidPoolTransactionError::Eip4844(error));
270 continue
271 }
272 };
273 }
274
275 let gas_used = match builder.execute_transaction(tx.clone()) {
276 Ok(gas_used) => gas_used,
277 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
278 error, ..
279 })) => {
280 if error.is_nonce_too_low() {
281 trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
283 } else {
284 trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
287 best_txs.mark_invalid(
288 &pool_tx,
289 InvalidPoolTransactionError::Consensus(
290 InvalidTransactionError::TxTypeNotSupported,
291 ),
292 );
293 }
294 continue
295 }
296 Err(err) => return Err(PayloadBuilderError::evm(err)),
298 };
299
300 if let Some(blob_tx) = tx.as_eip4844() {
302 block_blob_count += blob_tx.tx().blob_versioned_hashes.len() as u64;
303
304 if block_blob_count == max_blob_count {
306 best_txs.skip_blobs();
307 }
308 }
309
310 let miner_fee =
312 tx.effective_tip_per_gas(base_fee).expect("fee is always valid; execution succeeded");
313 total_fees += U256::from(miner_fee) * U256::from(gas_used);
314 cumulative_gas_used += gas_used;
315
316 if let Some(sidecar) = blob_tx_sidecar {
318 blob_sidecars.push_sidecar_variant(sidecar.as_ref().clone());
319 }
320 }
321
322 if !is_better_payload(best_payload.as_ref(), total_fees) {
324 drop(builder);
326 return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
328 }
329
330 let BlockBuilderOutcome { execution_result, block, .. } = builder.finish(&state_provider)?;
331
332 let requests = chain_spec
333 .is_prague_active_at_timestamp(attributes.timestamp)
334 .then_some(execution_result.requests);
335
336 let sealed_block = Arc::new(block.sealed_block().clone());
337 debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
338
339 let payload = EthBuiltPayload::new(attributes.id, sealed_block, total_fees, requests)
340 .with_sidecars(blob_sidecars);
342
343 Ok(BuildOutcome::Better { payload, cached_reads })
344}