Skip to content
Draft
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
2 changes: 1 addition & 1 deletion crates/networking/rpc/types/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub struct RpcReceipt {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcFrameReceipt {
/// EIP-8141 frame status code: 0 = failure, 1 = success, 3 = skipped
/// EIP-8141 frame status code: 0 = failure, 1 = success, 2 = skipped
/// (atomic-batch failure). Serialized as a hex-encoded byte.
#[serde(with = "serde_utils::u8::hex_str")]
pub status: u8,
Expand Down
17 changes: 12 additions & 5 deletions crates/vm/levm/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2857,13 +2857,19 @@ impl<'a> VM<'a> {
///
/// Static bans: `ORIGIN`, `GASPRICE`, `BLOCKHASH`, `COINBASE`, `TIMESTAMP`
/// (except when the current frame's target is EXPIRY_VERIFIER), `NUMBER`,
/// `PREVRANDAO`, `GASLIMIT`, `BASEFEE`, `BLOBHASH`, `BLOBBASEFEE`, `INVALID`,
/// `SELFDESTRUCT`, `BALANCE`, `SELFBALANCE`, `TLOAD`, `TSTORE`, and `CALLCODE`
/// in non-deploy prefix frames (ERC-7562 bans CALLCODE in validation;
/// `PREVRANDAO`, `GASLIMIT`, `BASEFEE`, `BLOBHASH`, `BLOBBASEFEE`, `SLOTNUM`,
/// `INVALID`, `SELFDESTRUCT`, `BALANCE`, `SELFBALANCE`, `TLOAD`, `TSTORE`, and
/// `CALLCODE` in non-deploy prefix frames (ERC-7562 bans CALLCODE in validation;
/// DELEGATECALL is allowed subject to the CALL-family trace rules in the
/// handlers). `SSTORE`/`CREATE`/`CREATE2` are allowed only inside the deploy
/// frame and are enforced in their handlers (state-write rules), not here.
///
/// `SLOTNUM` (EIP-7843) joins the block-dependent set for the same reason as
/// `NUMBER` and `TIMESTAMP`: its value changes between admission and inclusion,
/// so a prefix branching on it can pass simulation and revert in the block. It
/// is not covered transitively, since the handler reads the header's slot
/// number rather than deriving it from `TIMESTAMP`.
///
/// Sequential `GAS` rule: `GAS` is allowed only immediately before a
/// `*CALL` (`CALL`/`CALLCODE`/`DELEGATECALL`/`STATICCALL`). We detect this by
/// remembering `last_opcode`: if the previous iteration was `GAS` and this
Expand All @@ -2886,6 +2892,7 @@ impl<'a> VM<'a> {
const BASEFEE: u8 = 0x48;
const BLOBHASH: u8 = 0x49;
const BLOBBASEFEE: u8 = 0x4A;
const SLOTNUM: u8 = 0x4B;
const INVALID: u8 = 0xFE;
const SELFDESTRUCT: u8 = 0xFF;
const BALANCE: u8 = 0x31;
Expand All @@ -2912,8 +2919,8 @@ impl<'a> VM<'a> {

let banned = match opcode {
ORIGIN | GASPRICE | BLOCKHASH | COINBASE | NUMBER | PREVRANDAO | GASLIMIT | BASEFEE
| BLOBHASH | BLOBBASEFEE | INVALID | SELFDESTRUCT | BALANCE | SELFBALANCE | TLOAD
| TSTORE => true,
| BLOBHASH | BLOBBASEFEE | SLOTNUM | INVALID | SELFDESTRUCT | BALANCE | SELFBALANCE
| TLOAD | TSTORE => true,
// TIMESTAMP is permitted only when the currently executing contract
// IS the EXPIRY_VERIFIER predeploy (checked by code_address so the
// rule tracks the executing contract at every call depth, not just the
Expand Down
2 changes: 1 addition & 1 deletion docs/eip-8141.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ A frame tx carrying `blob_versioned_hashes` is rejected at mempool admission bec
The full ERC-7562-style validation-prefix simulation is implemented as a local peer policy. Admission runs the following checks in order, all in addition to the static and signature checks above:

1. **Prefix recognition and structural validation**: the frame list is walked to identify the recognized shapes (`self_verify`, `deploy+self_verify`, `only_verify+pay`, `deploy+only_verify+pay`), and structural rules (frame modes, scope restrictions, gas budget) are validated. Unrecognized or invalid prefixes are rejected at admission.
2. **Validation-trace simulation**: a dedicated `ValidationObserver` on the VM enforces ERC-7562-style banned-opcode and storage-dependency rules during a throwaway prefix-only execution against the canonical head state. Banned opcodes (TIMESTAMP outside the expiry verifier, BALANCE, SELFBALANCE, BLOCKHASH, ORIGIN, COINBASE, GASPRICE, NUMBER, PREVRANDAO, GASLIMIT, BASEFEE, BLOBHASH, BLOBBASEFEE, SELFDESTRUCT, INVALID, TLOAD, TSTORE; SSTORE/CREATE/CREATE2 only inside a deploy frame; GAS only immediately before a call instruction) cause the tx to be rejected with `FrameTxValidationFailed`.
2. **Validation-trace simulation**: a dedicated `ValidationObserver` on the VM enforces ERC-7562-style banned-opcode and storage-dependency rules during a throwaway prefix-only execution against the canonical head state. Banned opcodes (TIMESTAMP outside the expiry verifier, BALANCE, SELFBALANCE, BLOCKHASH, ORIGIN, COINBASE, GASPRICE, NUMBER, PREVRANDAO, GASLIMIT, BASEFEE, BLOBHASH, BLOBBASEFEE, SLOTNUM, SELFDESTRUCT, INVALID, TLOAD, TSTORE; SSTORE/CREATE/CREATE2 only inside a deploy frame; GAS only immediately before a call instruction) cause the tx to be rejected with `FrameTxValidationFailed`.
3. **Paymaster availability accounting**: the simulation identifies the payer (paymaster) and reserves the tx's `max_cost` against the paymaster's on-chain balance. Concurrent pending reservations are summed so multiple sponsored txs cannot collectively overdraw the paymaster.
4. **Post-block revalidation**: after each new canonical block, pending frame txs are re-simulated against the updated head state and evicted if they no longer pass. Expiry-based eviction (deadline now behind the new block's timestamp) runs first without a full re-simulation.

Expand Down
23 changes: 23 additions & 0 deletions test/tests/levm/eip8141_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1944,6 +1944,7 @@ mod validation_observer_tests {
assert_eq!(u8::from(Opcode::BASEFEE), 0x48);
assert_eq!(u8::from(Opcode::BLOBHASH), 0x49);
assert_eq!(u8::from(Opcode::BLOBBASEFEE), 0x4A);
assert_eq!(u8::from(Opcode::SLOTNUM), 0x4B);
assert_eq!(u8::from(Opcode::INVALID), 0xFE);
assert_eq!(u8::from(Opcode::SELFDESTRUCT), 0xFF);
assert_eq!(u8::from(Opcode::BALANCE), 0x31);
Expand Down Expand Up @@ -2178,6 +2179,28 @@ mod validation_observer_tests {
);
}

#[test]
fn slotnum_is_banned() {
let sender = addr(0x4B00);
// SLOTNUM (0x4B) then STOP. EIP-7843's slot number changes between
// admission and inclusion exactly like NUMBER/TIMESTAMP, so a prefix
// branching on it could pass simulation and revert on inclusion. It is
// not covered transitively: the handler reads `env.slot_number` from the
// header rather than deriving it from TIMESTAMP.
let code = Bytes::from(vec![0x4B, 0x00]);
let tx = frame_tx_for_obs(
sender,
vec![verify_frame_obs(sender, 50_000, 0x03, Bytes::new())],
);
let mut db = build_db(vec![(sender, account_with_code(0, code))]);
let (_result, violation) = run(&tx, &mut db, sender, &[0], None);
assert_eq!(
violation,
Some(FrameSimViolation::BannedOpcode(0x4B)),
"SLOTNUM must be a banned opcode during prefix simulation"
);
}

#[test]
fn sstore_outside_deploy_is_rejected() {
let sender = addr(0x55_00);
Expand Down
Loading