Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bin/morph-statetest/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ fn validation_error<E>(
where
E: std::fmt::Display,
{
// `expectException` is checked for presence, deliberately not for its text.
// go-ethereum's own statetest harness does the same -- `tests/state_test.go`
// returns early on `len(ExpectException) > 0` under a standing
// "TODO check error string" -- so matching on the text here would make this
// runner stricter than the client the fixtures are generated from, and a
// fixture imported from go-ethereum could fail on wording alone. The string
// stays in the JSON as documentation of which failure the case is meant to
// provoke; the assertion is that the transaction is *rejected*, which is what
// both clients agree on.
match (&test.expect_exception, exec_result) {
(Some(_), Err(_)) => return None,
(Some(expected), Ok(_)) => {
Expand Down
76 changes: 76 additions & 0 deletions bin/morph-statetest/tests/fee_token_internal_calls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Golden roots/logs generated by morph-geth (Emerald and Jade): the original twelve
//! cases on 5744b8f66, the three later ones on 4012f174b, which differs only in the
//! transaction-size limit in `core/tx_pool.go` and reproduces all twelve unchanged.
//! The gas constants are transaction totals; geth's statetest tool subtracts
//! intrinsic gas when the total is at least the intrinsic cost.
use morph_statetest::runner::run_suite_str;

#[test]
fn fee_token_calls_match_geth() {
let outcomes = run_suite_str(include_str!("fixtures/fee_token_internal_calls.json")).unwrap();
assert_eq!(outcomes.len(), 30);
for outcome in outcomes {
assert!(
outcome.pass,
"{} / {}: {}",
outcome.test, outcome.fork, outcome.error_msg
);
let gas = match outcome.test.as_str() {
"deduct_clear" => 16_800,
"main_revert" => 16_804,
"main_oog" => 95_200,
"main_restores_cleared_slot" => 23_291,
"balance_writes" => 0,
"zero_fee_reads_balance" => 23_574,
// The transaction's own frame reads MSIZE and MLOAD(0) into storage before
// writing either. go-ethereum allocates a fresh `Memory` per interpreter run,
// so it sees zeros and neither SSTORE changes state; a client whose fee frames
// leave their bytes on the transaction's memory writes two non-zero slots and
// misses this root by two SSTORE_SETs.
"main_reads_uninitialized_memory" => 25_417,
// The registry's direct-slot path. `slot_deduct_clear` is the same transaction
// as `deduct_clear`, which costs 16_800: clearing the payer's balance through a
// real `transfer` books a `+4800` SSTORE refund that reaches the transaction,
// while `SetState` books nothing. That 4_200 is the only way the two modes bill
// differently.
"slot_deduct_keep" | "slot_deduct_clear" => 21_000,
"deduct_keep"
| "origin_guard"
| "gasprice_guard"
| "refund_false_keeps_transfer"
| "zero_fee"
| "refund_revert_rolls_back_transfer" => 21_000,
name => panic!("missing gas expectation for {name}"),
};
assert_eq!(outcome.gas_used, gas, "{} / {}", outcome.test, outcome.fork);
}
}

/// Replays mainnet transaction `0x9ebfdac9040d7c2a8739ffdaae8baf5e7aa22fdb48585f80592de4b4cf39ed44`
/// (block 26836567, Jade): a MorphTx V0 that pays its fee in token 1, which the registry still
/// resolves through the direct-slot path, and whose call transfers that same token. The fixtures
/// above are synthetic; this is slot mode as mainnet actually ran it, which every node must keep
/// replaying identically after the registry moves off slot mode.
///
/// go-ethereum's state-test runner signs with `secretKey` and fixes the chain id to 1, so the
/// sender is the harness account `0xa94f…6ebf0b`, carrying the real sender's nonce, EIP-7702
/// delegation code and token balance under its own balance slot, and the fee vault's balance sits
/// under the harness vault. The prestate tracer does not see the slots the fee logic reads straight
/// from state, so the token balances, the registry entry and the L1 gas price oracle come from the
/// parent block, which is exact here because the transaction is alone in its block. Roots are from
/// morph-geth 4012f174b. The logs root equals the on-chain receipt's logs with the sender topic
/// substituted, and the gas matches the on-chain receipt: the substitution changes the L1 data fee,
/// and with it the token amount charged, but not the gas burned.
#[test]
fn mainnet_slot_mode_fee_token_transfer_matches_geth() {
let outcomes =
run_suite_str(include_str!("fixtures/mainnet_slot_mode_fee_token.json")).unwrap();
assert_eq!(outcomes.len(), 1);
let outcome = &outcomes[0];
assert!(
outcome.pass,
"{} / {}: {}",
outcome.test, outcome.fork, outcome.error_msg
);
assert_eq!(outcome.gas_used, 51_257);
}
Loading
Loading