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