use std::{collections::BTreeMap, fmt::Display};
use alloy_consensus::{BlockHeader, Transaction as _};
use alloy_eips::Typed2718 as _;
use alloy_primitives::KECCAK256_EMPTY;
use alloy_rpc_types_eth::Block;
use mega_evm::{
revm::{
context::result::ExecutionResult,
primitives::{Address, Bytes, B256, U256},
state::EvmState,
DatabaseRef,
},
MegaHaltReason, MegaSpecId,
};
use op_alloy_consensus::OpTxEnvelope;
use op_alloy_rpc_types::Transaction;
use state_test::{
runner::{execute_unit_collect, execution_status, halt_reason},
types::{AccountInfo, Env, MegaEnv, SpecName, Test, TestSuite, TestUnit, TransactionParts},
};
use super::{ReplayError, Result};
pub(crate) struct OnchainAnchor {
pub gas_used: u64,
pub success: bool,
pub logs_root: B256,
}
pub(crate) struct FixtureInputs<'a> {
pub mega_env: MegaEnv,
pub result: &'a ExecutionResult<MegaHaltReason>,
pub anchor: OnchainAnchor,
}
const DEPOSIT_TX_TYPE: u8 = 0x7e;
const EIP7702_TX_TYPE: u8 = 0x04;
pub(crate) struct FixtureDraft {
unit: TestUnit,
spec: SpecName,
actual_gas: u64,
actual_status: String,
actual_halt_reason: Option<String>,
actual_output: Option<Bytes>,
actual_logs_root: B256,
name: String,
}
pub(crate) fn build_draft<DB>(
db: &DB,
evm_state: &EvmState,
chain_id: u64,
spec: MegaSpecId,
block: &Block<Transaction>,
target_tx: &Transaction,
inputs: FixtureInputs<'_>,
) -> Result<FixtureDraft>
where
DB: DatabaseRef,
DB::Error: Display,
{
let envelope: &OpTxEnvelope = &target_tx.inner.inner;
if envelope.ty() == DEPOSIT_TX_TYPE {
return Err(ReplayError::Other(
"--dump-fixture does not support deposit transactions".to_string(),
));
}
if envelope.ty() == EIP7702_TX_TYPE {
return Err(ReplayError::Other(
"--dump-fixture does not support EIP-7702 (set-code) transactions: the \
fixture builder does not serialize the authorization list"
.to_string(),
));
}
let actual_gas = inputs.result.gas_used();
let actual_status = execution_status(inputs.result).to_string();
let actual_halt_reason = halt_reason(inputs.result);
let actual_output = inputs.result.output().cloned();
let actual_logs_root = state_test::utils::log_rlp_hash(inputs.result.logs());
let anchor = &inputs.anchor;
if actual_gas != anchor.gas_used {
return Err(ReplayError::Other(format!(
"replay gas {actual_gas} != on-chain receipt gas {}: the local replay does \
not reproduce on-chain execution (likely a wrong spec or hardfork config \
for chain {chain_id} at this block)",
anchor.gas_used
)));
}
if inputs.result.is_success() != anchor.success {
return Err(ReplayError::Other(format!(
"replay status (success={}) != on-chain receipt status (success={}): the \
local replay does not reproduce on-chain execution for chain {chain_id}",
inputs.result.is_success(),
anchor.success
)));
}
if actual_logs_root != anchor.logs_root {
return Err(ReplayError::Other(format!(
"replay logs root {actual_logs_root} != on-chain receipt logs root {}: the \
local replay emits different logs than the chain for chain {chain_id} \
(same gas/status, different log contents)",
anchor.logs_root
)));
}
let pre = build_pre_state(db, evm_state)?;
let env = build_env(chain_id, block);
let transaction = build_transaction(target_tx)?;
let spec_name = SpecName::from_mega_spec(spec);
if spec_name == SpecName::Unknown {
return Err(ReplayError::Other(format!(
"--dump-fixture: spec {spec:?} has no fixture mapping"
)));
}
let unit = TestUnit {
info: None,
env,
pre,
post: BTreeMap::new(),
transaction,
out: None,
mega_env: Some(inputs.mega_env),
extra: BTreeMap::new(),
};
let name = format!("replay_{:#x}", target_tx.inner.inner.tx_hash());
Ok(FixtureDraft {
unit,
spec: spec_name,
actual_gas,
actual_status,
actual_halt_reason,
actual_output,
actual_logs_root,
name,
})
}
pub(crate) fn finalize_and_write(draft: FixtureDraft, path: &std::path::Path) -> Result<()> {
let executed = execute_unit_collect(&draft.unit, &draft.spec)
.map_err(|e| ReplayError::Other(format!("fixture self-execution failed: {e}")))?;
if executed.gas_used != draft.actual_gas {
return Err(ReplayError::Other(format!(
"fixture not reproducible: gas {} != replay gas {} (incomplete pre-state?)",
executed.gas_used, draft.actual_gas
)));
}
if executed.status != draft.actual_status {
return Err(ReplayError::Other(format!(
"fixture not reproducible: status {:?} != replay status {:?}",
executed.status, draft.actual_status
)));
}
if executed.halt_reason != draft.actual_halt_reason {
return Err(ReplayError::Other(format!(
"fixture not reproducible: halt reason {:?} != replay halt reason {:?}",
executed.halt_reason, draft.actual_halt_reason
)));
}
if executed.output != draft.actual_output {
return Err(ReplayError::Other(
"fixture not reproducible: output differs from replay".to_string(),
));
}
if executed.logs_root != draft.actual_logs_root {
return Err(ReplayError::Other(format!(
"fixture not reproducible: logs root {} != replay logs root {} \
(isolated run diverges from the full replay, e.g. via the L1 data fee)",
executed.logs_root, draft.actual_logs_root
)));
}
let mut unit = draft.unit;
unit.out = executed.output.clone();
let test =
Test::for_dump(executed.state_root, executed.logs_root, executed.gas_used, executed.status);
unit.post = BTreeMap::from([(draft.spec, vec![test])]);
let suite = TestSuite(BTreeMap::from([(draft.name, unit)]));
let json = serde_json::to_string_pretty(&suite)
.map_err(|e| ReplayError::Other(format!("failed to serialize fixture: {e}")))?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, json).map_err(|e| {
ReplayError::Other(format!("failed to write fixture {}: {e}", tmp.display()))
})?;
std::fs::rename(&tmp, path).map_err(|e| {
ReplayError::Other(format!(
"failed to rename fixture {} -> {}: {e}",
tmp.display(),
path.display()
))
})
}
fn build_pre_state<DB>(db: &DB, evm_state: &EvmState) -> Result<BTreeMap<Address, AccountInfo>>
where
DB: DatabaseRef,
DB::Error: Display,
{
let mut pre = BTreeMap::new();
for (address, account) in evm_state {
let Some(info) = db
.basic_ref(*address)
.map_err(|e| ReplayError::Other(format!("pre-state read for {address}: {e}")))?
else {
continue;
};
let code = resolve_code(db, info.code_hash, info.code.as_ref())?;
let storage: BTreeMap<U256, U256> = account
.storage
.iter()
.filter(|(_, slot)| !slot.original_value.is_zero())
.map(|(key, slot)| (*key, slot.original_value))
.collect();
pre.insert(
*address,
AccountInfo { balance: info.balance, code, nonce: info.nonce, storage },
);
}
Ok(pre)
}
fn resolve_code<DB>(
db: &DB,
code_hash: B256,
code: Option<&mega_evm::revm::state::Bytecode>,
) -> Result<Bytes>
where
DB: DatabaseRef,
DB::Error: Display,
{
if let Some(bytecode) = code {
return Ok(bytecode.original_byte_slice().to_vec().into());
}
if code_hash == KECCAK256_EMPTY {
return Ok(Bytes::new());
}
let bytecode = db
.code_by_hash_ref(code_hash)
.map_err(|e| ReplayError::Other(format!("code fetch for {code_hash}: {e}")))?;
Ok(bytecode.original_byte_slice().to_vec().into())
}
fn build_env(chain_id: u64, block: &Block<Transaction>) -> Env {
let header = &block.header;
Env {
current_chain_id: Some(U256::from(chain_id)),
current_coinbase: header.beneficiary(),
current_difficulty: header.difficulty(),
current_gas_limit: U256::from(header.gas_limit()),
current_number: U256::from(header.number()),
current_timestamp: U256::from(header.timestamp()),
current_base_fee: header.base_fee_per_gas().map(U256::from),
current_random: header.mix_hash().map(|h| U256::from_be_bytes(h.0)),
current_excess_blob_gas: header.excess_blob_gas().map(U256::from),
previous_hash: None,
parent_timestamp: None,
parent_gas_used: None,
parent_gas_limit: None,
parent_base_fee: None,
parent_hash: None,
parent_uncle_hash: None,
parent_beacon_block_root: None,
parent_difficulty: None,
block_hashes: None,
ommers: None,
withdrawals: None,
current_beacon_root: None,
current_withdrawals_root: None,
parent_blob_gas_used: None,
parent_excess_blob_gas: None,
parent_target_blobs_per_block: None,
current_blob_gas_used: None,
}
}
fn build_transaction(target_tx: &Transaction) -> Result<TransactionParts> {
let sender = target_tx.inner.inner.signer();
let tx: &OpTxEnvelope = &target_tx.inner.inner;
let tx_type = tx.ty();
let (gas_price, max_fee_per_gas) = match tx_type {
0 | 1 => {
let gas_price = tx.gas_price().ok_or_else(|| {
ReplayError::Other(format!(
"--dump-fixture: transaction type {tx_type} reports no gas price; \
refusing to record a guessed price in the fixture"
))
})?;
(Some(U256::from(gas_price)), None)
}
_ => (None, Some(U256::from(tx.max_fee_per_gas()))),
};
Ok(TransactionParts {
tx_type: Some(tx_type),
data: vec![tx.input().clone()],
gas_limit: vec![U256::from(tx.gas_limit())],
gas_price,
nonce: U256::from(tx.nonce()),
secret_key: B256::ZERO,
sender: Some(sender),
to: tx.to(),
value: vec![tx.value()],
max_fee_per_gas,
max_priority_fee_per_gas: tx.max_priority_fee_per_gas().map(U256::from),
initcodes: None,
access_lists: vec![tx.access_list().cloned()],
authorization_list: None,
blob_versioned_hashes: tx.blob_versioned_hashes().map(|h| h.to_vec()).unwrap_or_default(),
max_fee_per_blob_gas: tx.max_fee_per_blob_gas().map(U256::from),
})
}