Skip to content

Commit a708137

Browse files
authored
Fix external payload validation logic, resolve txs execution logic bug and add pre-exec validation (#341)
* Add external payload validation logic inside fb payload handler (#26) * Remove post execution validation as default already does the validation (#27) * Revert comments (#28) * Fix execution txs logic on external payload validation (#31) * Revert comment
1 parent 898ec7a commit a708137

2 files changed

Lines changed: 88 additions & 106 deletions

File tree

crates/op-rbuilder/src/builders/flashblocks/ctx.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use reth_basic_payload_builder::PayloadConfig;
99
use reth_evm::EvmEnv;
1010
use reth_optimism_chainspec::OpChainSpec;
1111
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
12+
use reth_optimism_forks::OpHardforks;
1213
use reth_optimism_payload_builder::{
1314
OpPayloadBuilderAttributes,
1415
config::{OpDAConfig, OpGasLimitConfig},
@@ -59,6 +60,16 @@ impl OpPayloadSyncerCtx {
5960
self.max_gas_per_txn
6061
}
6162

63+
/// Returns true if regolith is active for the payload.
64+
pub(super) fn is_regolith_active(&self, timestamp: u64) -> bool {
65+
self.chain_spec.is_regolith_active_at_timestamp(timestamp)
66+
}
67+
68+
/// Returns true if canyon is active for the payload.
69+
pub(super) fn is_canyon_active(&self, timestamp: u64) -> bool {
70+
self.chain_spec.is_canyon_active_at_timestamp(timestamp)
71+
}
72+
6273
pub(super) fn into_op_payload_builder_ctx(
6374
self,
6475
payload_config: PayloadConfig<OpPayloadBuilderAttributes<OpTransactionSigned>>,

crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs

Lines changed: 77 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ use crate::{
88
use alloy_evm::eth::receipt_builder::ReceiptBuilderCtx;
99
use alloy_primitives::B64;
1010
use eyre::{WrapErr as _, bail};
11-
use op_alloy_consensus::OpTxEnvelope;
1211
use op_alloy_rpc_types_engine::OpFlashblockPayload;
1312
use op_revm::L1BlockInfo;
1413
use reth::revm::{State, database::StateProviderDatabase};
1514
use reth_basic_payload_builder::PayloadConfig;
16-
use reth_evm::FromRecoveredTx;
1715
use reth_node_builder::Events;
1816
use reth_optimism_chainspec::OpChainSpec;
19-
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
17+
use reth_optimism_consensus::OpBeaconConsensus;
18+
use reth_optimism_evm::OpNextBlockEnvAttributes;
2019
use reth_optimism_forks::OpHardforks;
2120
use reth_optimism_node::{OpEngineTypes, OpPayloadBuilderAttributes};
2221
use reth_optimism_payload_builder::OpBuiltPayload;
2322
use reth_optimism_primitives::{OpReceipt, OpTransactionSigned};
2423
use reth_payload_builder::EthPayloadBuilderAttributes;
24+
use reth_primitives_traits::SealedHeader;
2525
use std::sync::Arc;
2626
use tokio::sync::mpsc;
2727
use tracing::warn;
@@ -156,6 +156,11 @@ where
156156
.wrap_err("failed to get parent header")?
157157
.ok_or_else(|| eyre::eyre!("parent header not found"))?;
158158

159+
// For X Layer, validate header and parent relationship before execution
160+
let chain_spec = client.chain_spec();
161+
validate_pre_execution(&payload, &parent_header, parent_hash, chain_spec.clone())
162+
.wrap_err("pre-execution validation failed")?;
163+
159164
let state_provider = client
160165
.state_by_block_hash(parent_hash)
161166
.wrap_err("failed to get state for parent hash")?;
@@ -165,7 +170,6 @@ where
165170
.with_bundle_update()
166171
.build();
167172

168-
let chain_spec = client.chain_spec();
169173
let timestamp = payload.block().header().timestamp();
170174
let block_env_attributes = OpNextBlockEnvAttributes {
171175
timestamp,
@@ -249,17 +253,14 @@ where
249253
);
250254

251255
execute_transactions(
256+
&ctx,
252257
&mut info,
253258
&mut state,
254259
payload.block().body().transactions.clone(),
255260
payload.block().header().gas_used,
256261
timestamp,
257-
ctx.evm_config(),
258262
evm_env.clone(),
259263
chain_spec.clone(),
260-
ctx.max_gas_per_txn(),
261-
is_canyon_active(&chain_spec, timestamp),
262-
is_regolith_active(&chain_spec, timestamp),
263264
)
264265
.wrap_err("failed to execute best transactions")?;
265266

@@ -301,91 +302,56 @@ where
301302

302303
#[allow(clippy::too_many_arguments)]
303304
fn execute_transactions(
305+
ctx: &OpPayloadSyncerCtx,
304306
info: &mut ExecutionInfo<FlashblocksExecutionInfo>,
305307
state: &mut State<impl alloy_evm::Database>,
306308
txs: Vec<op_alloy_consensus::OpTxEnvelope>,
307309
gas_limit: u64,
308310
timestamp: u64,
309-
evm_config: &reth_optimism_evm::OpEvmConfig,
310311
evm_env: alloy_evm::EvmEnv<op_revm::OpSpecId>,
311312
chain_spec: Arc<OpChainSpec>,
312-
max_gas_per_txn: Option<u64>,
313-
is_canyon_active: bool,
314-
is_regolith_active: bool,
315313
) -> eyre::Result<()> {
316-
use alloy_evm::{Evm as _, EvmError as _};
317-
use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts};
314+
use alloy_evm::Evm as _;
318315
use reth_evm::ConfigureEvm as _;
319-
use reth_primitives_traits::SignerRecoverable as _;
320-
use revm::{
321-
DatabaseCommit as _,
322-
context::{TxEnv, result::ResultAndState},
323-
};
316+
use reth_primitives_traits::SignedTransaction;
317+
use revm::{DatabaseCommit as _, context::result::ResultAndState};
324318

325-
let mut evm = evm_config.evm_with_env(&mut *state, evm_env);
319+
let mut evm = ctx.evm_config().evm_with_env(&mut *state, evm_env);
326320

327321
for tx in txs {
328-
let sender = tx
329-
.recover_signer()
330-
.wrap_err("failed to recover tx signer")?;
331-
let tx_env = TxEnv::from_recovered_tx(&tx, sender);
332-
let executable_tx = match tx {
333-
OpTxEnvelope::Deposit(ref tx) => {
334-
let deposit = DepositTransactionParts {
335-
mint: Some(tx.mint),
336-
source_hash: tx.source_hash,
337-
is_system_transaction: tx.is_system_transaction,
338-
};
339-
OpTransaction {
340-
base: tx_env,
341-
enveloped_tx: None,
342-
deposit,
343-
}
344-
}
345-
OpTxEnvelope::Legacy(_) => {
346-
let mut tx = OpTransaction::new(tx_env);
347-
tx.enveloped_tx = Some(vec![0x00].into());
348-
tx
349-
}
350-
OpTxEnvelope::Eip2930(_) => {
351-
let mut tx = OpTransaction::new(tx_env);
352-
tx.enveloped_tx = Some(vec![0x00].into());
353-
tx
354-
}
355-
OpTxEnvelope::Eip1559(_) => {
356-
let mut tx = OpTransaction::new(tx_env);
357-
tx.enveloped_tx = Some(vec![0x00].into());
358-
tx
359-
}
360-
OpTxEnvelope::Eip7702(_) => {
361-
let mut tx = OpTransaction::new(tx_env);
362-
tx.enveloped_tx = Some(vec![0x00].into());
363-
tx
364-
}
365-
};
322+
// Convert to recovered transaction
323+
let tx_recovered = tx
324+
.try_clone_into_recovered()
325+
.wrap_err("failed to recover tx")?;
326+
let sender = tx_recovered.signer();
327+
328+
// Cache the depositor account prior to the state transition for the deposit nonce.
329+
//
330+
// Note that this *only* needs to be done post-regolith hardfork, as deposit nonces
331+
// were not introduced in Bedrock. In addition, regular transactions don't have deposit
332+
// nonces, so we don't need to touch the DB for those.
333+
let depositor_nonce = (ctx.is_regolith_active(timestamp) && tx_recovered.is_deposit())
334+
.then(|| {
335+
evm.db_mut()
336+
.load_cache_account(sender)
337+
.map(|acc| acc.account_info().unwrap_or_default().nonce)
338+
})
339+
.transpose()
340+
.wrap_err("failed to get depositor nonce")?;
366341

367-
let ResultAndState { result, state } = match evm.transact_raw(executable_tx) {
368-
Ok(res) => res,
369-
Err(err) => {
370-
if let Some(err) = err.as_invalid_tx_err() {
371-
// TODO: what invalid txs are allowed in the block?
372-
// reverting txs should be allowed (?) but not straight up invalid ones
373-
tracing::error!(error = %err, "skipping invalid transaction in flashblock");
374-
continue;
375-
}
376-
return Err(err).wrap_err("failed to execute flashblock transaction");
377-
}
378-
};
342+
let ResultAndState { result, state } = evm
343+
.transact(&tx_recovered)
344+
.wrap_err("failed to execute transaction")?;
379345

380-
if let Some(max_gas_per_txn) = max_gas_per_txn
381-
&& result.gas_used() > max_gas_per_txn
346+
let tx_gas_used = result.gas_used();
347+
if let Some(max_gas_per_txn) = ctx.max_gas_per_txn()
348+
&& tx_gas_used > max_gas_per_txn
382349
{
383350
return Err(eyre::eyre!(
384351
"transaction exceeded max gas per txn limit in flashblock"
385352
));
386353
}
387354

388-
let tx_gas_used = result.gas_used();
389355
info.cumulative_gas_used = info
390356
.cumulative_gas_used
391357
.checked_add(tx_gas_used)
@@ -396,29 +362,16 @@ fn execute_transactions(
396362
bail!("flashblock exceeded gas limit when executing transactions");
397363
}
398364

399-
let depositor_nonce = (is_regolith_active && tx.is_deposit())
400-
.then(|| {
401-
evm.db_mut()
402-
.load_cache_account(sender)
403-
.map(|acc| acc.account_info().unwrap_or_default().nonce)
404-
})
405-
.transpose()
406-
.wrap_err("failed to get depositor nonce")?;
407-
408-
let ctx = ReceiptBuilderCtx {
365+
let receipt_ctx = ReceiptBuilderCtx {
409366
tx: &tx,
410367
evm: &evm,
411368
result,
412369
state: &state,
413370
cumulative_gas_used: info.cumulative_gas_used,
414371
};
415372

416-
info.receipts.push(build_receipt(
417-
evm_config,
418-
ctx,
419-
depositor_nonce,
420-
is_canyon_active,
421-
));
373+
info.receipts
374+
.push(build_receipt(ctx, receipt_ctx, depositor_nonce, timestamp));
422375

423376
evm.db_mut().commit(state);
424377

@@ -440,26 +393,26 @@ fn execute_transactions(
440393
}
441394

442395
fn build_receipt<E: alloy_evm::Evm>(
443-
evm_config: &OpEvmConfig,
444-
ctx: ReceiptBuilderCtx<'_, OpTransactionSigned, E>,
396+
ctx: &OpPayloadSyncerCtx,
397+
receipt_ctx: ReceiptBuilderCtx<'_, OpTransactionSigned, E>,
445398
deposit_nonce: Option<u64>,
446-
is_canyon_active: bool,
399+
timestamp: u64,
447400
) -> OpReceipt {
448401
use alloy_consensus::Eip658Value;
449402
use alloy_op_evm::block::receipt_builder::OpReceiptBuilder as _;
450403
use op_alloy_consensus::OpDepositReceipt;
451404
use reth_evm::ConfigureEvm as _;
452405

453-
let receipt_builder = evm_config.block_executor_factory().receipt_builder();
454-
match receipt_builder.build_receipt(ctx) {
406+
let receipt_builder = ctx.evm_config().block_executor_factory().receipt_builder();
407+
match receipt_builder.build_receipt(receipt_ctx) {
455408
Ok(receipt) => receipt,
456-
Err(ctx) => {
409+
Err(receipt_ctx) => {
457410
let receipt = alloy_consensus::Receipt {
458411
// Success flag was added in `EIP-658: Embedding transaction status code
459412
// in receipts`.
460-
status: Eip658Value::Eip658(ctx.result.is_success()),
461-
cumulative_gas_used: ctx.cumulative_gas_used,
462-
logs: ctx.result.into_logs(),
413+
status: Eip658Value::Eip658(receipt_ctx.result.is_success()),
414+
cumulative_gas_used: receipt_ctx.cumulative_gas_used,
415+
logs: receipt_ctx.result.into_logs(),
463416
};
464417

465418
receipt_builder.build_deposit_receipt(OpDepositReceipt {
@@ -470,18 +423,36 @@ fn build_receipt<E: alloy_evm::Evm>(
470423
// when set. The state transition process ensures
471424
// this is only set for post-Canyon deposit
472425
// transactions.
473-
deposit_receipt_version: is_canyon_active.then_some(1),
426+
deposit_receipt_version: ctx.is_canyon_active(timestamp).then_some(1),
474427
})
475428
}
476429
}
477430
}
478431

479-
fn is_canyon_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool {
480-
use reth_optimism_chainspec::OpHardforks as _;
481-
chain_spec.is_canyon_active_at_timestamp(timestamp)
482-
}
432+
/// Validates the payload header and its relationship with the parent before execution.
433+
/// This performs consensus rule validation including:
434+
/// - Header field validation (timestamp, gas limit, etc.)
435+
/// - Parent relationship validation (block number increment, timestamp progression)
436+
fn validate_pre_execution(
437+
payload: &OpBuiltPayload,
438+
parent_header: &reth_primitives_traits::Header,
439+
parent_hash: alloy_primitives::B256,
440+
chain_spec: Arc<OpChainSpec>,
441+
) -> eyre::Result<()> {
442+
use reth::consensus::HeaderValidator;
443+
444+
let consensus = OpBeaconConsensus::new(chain_spec);
445+
let parent_sealed = SealedHeader::new(parent_header.clone(), parent_hash);
446+
447+
// Validate incoming header
448+
consensus
449+
.validate_header(payload.block().sealed_header())
450+
.wrap_err("header validation failed")?;
483451

484-
fn is_regolith_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool {
485-
use reth_optimism_chainspec::OpHardforks as _;
486-
chain_spec.is_regolith_active_at_timestamp(timestamp)
452+
// Validate incoming header against parent
453+
consensus
454+
.validate_header_against_parent(payload.block().sealed_header(), &parent_sealed)
455+
.wrap_err("header validation against parent failed")?;
456+
457+
Ok(())
487458
}

0 commit comments

Comments
 (0)