Skip to content

Conversation

@sieniven
Copy link
Contributor

@sieniven sieniven commented Dec 3, 2025

📝 Summary

This PR resolve issues found on the payload handler when handling received externally built flashblocks payload. It is found that when retrieving external payloads from peer builders, the state root calculation could mismatch due to errors on the payload validation execution (refer to Issue logs section for the example logs).

  1. Resolve issue where retrieval depositor nonce was after tx execution - depositor nonce should be cached and retrieved before the transaction execution
  2. Use the standard reth's execution pattern evm.transact() instead for execution of transactions and handling of state changes. We should pass the tx value itself directly into evm.transact, allowing the safe conversion into the tx env interface (for revm execution) using upstream conversion methods
  3. Add pre-execution payload validation on the incoming block header and parent block header validations, aligning with the default reth's payload_validator logic
  4. Chore: switch repeated logs to debug level

Issue logs

2025-12-03T06:24:06.957584Z INFO Received block from consensus engine number=8594186 hash=0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8
2025-12-03T06:24:06.958276Z INFO Canonical chain committed number=8594186 hash=0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8 elapsed=56.792µs
2025-12-03T06:24:07.959807Z INFO executing flashblock header=Header { parent_hash: 0x92a82266795a15929a3ed696fe1e362172b1d63d50baf46beefa7342329200a8, ommers_hash: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347, beneficiary: 0x4200000000000000000000000000000000000011, state_root: 0xddfd9afcfde543996f2d18e8f5c62e57d232e0f0e9064940edaaa2d224871237, transactions_root: 0x83d6757ffa229f27347d6c365e942198163325eb8c1b1a5c159a9d844422b581, receipts_root: 0xfc1d8bc99328e4113633b9ab891882bac50660e419d13c144d943f7c811e38d1, logs_bloom: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, difficulty: 0, number: 8594187, gas_limit: 150000000, gas_used: 147220286, timestamp: 1764743048, extra_data: 0x00000000fa00000006, mix_hash: 0x9d542a3f5333b4b3f8223c60b866bc33a4fd4be8ee3e4b3e7c6cb527b6ec18fa, nonce: 0x0000000000000000, base_fee_per_gas: Some(345052862), withdrawals_root: Some(0x8ed4baae3a927be3dea54996b4d5899f8c01e7594bf50b17dc1e741388ce3d12), blob_gas_used: Some(0), excess_blob_gas: Some(0), parent_beacon_block_root: Some(0x8b8d851a12382805c09ed58c23047e2888b3f6649d9b3952383922b89e68efe6), requests_hash: Some(0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) }
2025-12-03T06:24:07.983973Z INFO Received block from consensus engine number=8594187 hash=0x028840ebb9422a1f70ccb6b92ac91b99ba0c714484f2914f8fed9235b84e32e7
2025-12-03T06:24:08.058053Z ERROR flashblock hash mismatch after execution expected=0x028840ebb9422a1f70ccb6b92ac91b99ba0c714484f2914f8fed9235b84e32e7 got=0xfbf5b5d69ff651ce69d1028c6e5912f471d565290378d1d91e6827b053535b9e
2025-12-03T06:24:08.058826Z ERROR failed to execute received flashblock error=flashblock hash mismatch after execution

✅ I have completed the following steps:

  • [✅] Run make lint
  • [✅] Run make test
  • Added tests (if applicable)

@SozinM
Copy link
Collaborator

SozinM commented Dec 11, 2025

@noot

@sieniven
Copy link
Contributor Author

@noot @SozinM hey guys, any updates for this PR?

@noot
Copy link
Contributor

noot commented Jan 15, 2026

hi @sieniven thank you for the fixes, apologies for the slow response! are you able to share the steps you used to trigger the hash mismatch error? this feature is very experimental and has only been tested locally, not on any sort of production environment. it would be good to have the cases you used for testing documented.

@sieniven
Copy link
Contributor Author

hi @sieniven thank you for the fixes, apologies for the slow response! are you able to share the steps you used to trigger the hash mismatch error? this feature is very experimental and has only been tested locally, not on any sort of production environment. it would be good to have the cases you used for testing documented.

Hey @noot, thanks for taking the time to explain - this makes sense. The hash mismatch error occur in 2 scenarios. I'll comment directly on the code blocks for your reference.

Comment on lines -297 to -365
let sender = tx
.recover_signer()
.wrap_err("failed to recover tx signer")?;
let tx_env = TxEnv::from_recovered_tx(&tx, sender);
let executable_tx = match tx {
OpTxEnvelope::Deposit(ref tx) => {
let deposit = DepositTransactionParts {
mint: Some(tx.mint),
source_hash: tx.source_hash,
is_system_transaction: tx.is_system_transaction,
};
OpTransaction {
base: tx_env,
enveloped_tx: None,
deposit,
}
}
OpTxEnvelope::Legacy(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip2930(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip1559(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
OpTxEnvelope::Eip7702(_) => {
let mut tx = OpTransaction::new(tx_env);
tx.enveloped_tx = Some(vec![0x00].into());
tx
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first problematic area that the PR resolves is here - there shouldnt be a need to write local definition of convertion methods to alloy typed tx types, but instead pass the tx value itself directly into evm.transact, which allows the safe conversion into the tx env interface (for revm execution) using upstream conversion methods.

.transpose()
.wrap_err("failed to get depositor nonce")?;

let ResultAndState { result, state } = match evm.transact_raw(executable_tx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use the upstream's default pattern, evm.transact here which inherently calls IntoTxEnv on the concrete type for tx execution.

Comment on lines -368 to -406
let depositor_nonce = (is_regolith_active && tx.is_deposit())
.then(|| {
evm.db_mut()
.load_cache_account(sender)
.map(|acc| acc.account_info().unwrap_or_default().nonce)
})
.transpose()
.wrap_err("failed to get depositor nonce")?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second issue (and where the block hash mismatch is) is located here. The depositor nonce when bulding the receipt should be using the nonce prior to execution. However, the deposit tx receipt uses the depositor nonce after execution, which will cause a stateroot mismatch on the incorrect block data

Comment on lines 394 to 419
/// Validates the payload header and its relationship with the parent before execution.
/// This performs consensus rule validation including:
/// - Header field validation (timestamp, gas limit, etc.)
/// - Parent relationship validation (block number increment, timestamp progression)
fn validate_pre_execution(
payload: &OpBuiltPayload,
parent_header: &reth_primitives_traits::Header,
parent_hash: alloy_primitives::B256,
chain_spec: Arc<OpChainSpec>,
) -> eyre::Result<()> {
use reth::consensus::HeaderValidator;

fn is_regolith_active(chain_spec: &OpChainSpec, timestamp: u64) -> bool {
use reth_optimism_chainspec::OpHardforks as _;
chain_spec.is_regolith_active_at_timestamp(timestamp)
let consensus = OpBeaconConsensus::new(chain_spec);
let parent_sealed = SealedHeader::new(parent_header.clone(), parent_hash);

// Validate incoming header
consensus
.validate_header(payload.block().sealed_header())
.wrap_err("header validation failed")?;

// Validate incoming header against parent
consensus
.validate_header_against_parent(payload.block().sealed_header(), &parent_sealed)
.wrap_err("header validation against parent failed")?;

Ok(())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3rd issue is that in the payload validation process on default reth, we should pre-validate the incoming payload first prior to execution. This includes validating incoming payload header first which is missing in this p2p builder validation logic - https://github.com/okx/reth/blob/upstream/dev/crates/engine/tree/src/tree/payload_validator.rs#L27

Copy link
Contributor

@noot noot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good!

@noot
Copy link
Contributor

noot commented Jan 19, 2026

@sieniven thank you, the changes look good to me! in terms of what case you used to trigger the hash mismatch, i am assuming you used deposit transactions to cause the issue? i would also be curious to discuss how you are using this feature and if you are planning to use it in prod. we are not actively working on this feature at the moment and don't really have plans to do so, so if you are planning to use it, i would like to figure out how we can support that.

@sieniven
Copy link
Contributor Author

sieniven commented Jan 20, 2026

@noot thanks for the review! Yeah the issue can easily be recreated by running any optimism stack based chain, which the mismatch will happen on the default system tx (deposit tx type).

Regarding our use case - actually we use the p2p functionality in production because we directly integrate the flashblocks builder into our X Layer sequencer, which we have found to greatly optimize the throughput of the chain (due to some inherent design tradeoffs when separating the builder and proposal with rollup-boost running). As such, we rely on the p2p functionality to do 2 things:

  1. Propagate newly minted flashblock payloads to other peer sequencers in a multi-sequencers setup, so that other subscribers of follower sequencers will still be gossiped newly minted flashblocks
  2. Optimistically pre-warm our engine state tree (after validating the full execution payload), so that follower sequencers will have minimal latencies on following the leader sequencer (this is the same behaviour on your version). However this feature is also turned off currently in production on our side

Our op-rbuilder code differs abit here, I was intending to open another PR to you guys support this after this PR is merged. Maybe you guys can discuss internally if it makes sense to support this? 👍

Code ref: https://github.com/okx/op-rbuilder/blob/d35d855fc25458b846d0d80aabf47325eed4028e/crates/op-rbuilder/src/builders/flashblocks/payload_handler.rs#L108-L123
Note that the code block here contains 2 different payload receivers, which I have already opened this PR in #354.

cc/ @SozinM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants