Skip to content

Commit 9d33c46

Browse files
fix(l1): report status 2 for skipped EIP-8141 frames
1 parent cee872d commit 9d33c46

7 files changed

Lines changed: 117 additions & 12 deletions

File tree

crates/common/types/receipt.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ use crate::types::TxType;
1313
pub type Index = u64;
1414

1515
/// Frame receipt status codes (EIP-8141).
16-
/// `0x3` is reserved for frames skipped due to a failed atomic batch.
16+
/// `0x2` is reserved for frames skipped due to a failed atomic batch.
1717
pub const FRAME_RECEIPT_STATUS_FAILURE: u8 = 0;
1818
pub const FRAME_RECEIPT_STATUS_SUCCESS: u8 = 1;
19-
pub const FRAME_RECEIPT_STATUS_SKIPPED: u8 = 3;
19+
pub const FRAME_RECEIPT_STATUS_SKIPPED: u8 = 2;
2020

2121
/// Per-frame execution result within a frame transaction (EIP-8141)
2222
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
@@ -744,16 +744,25 @@ mod test {
744744

745745
#[test]
746746
fn test_frame_receipt_skipped_status_rlp_roundtrip() {
747-
// Spec line 137: status code 0x3 marks frames skipped by a failed atomic batch.
747+
// Spec line 137: status code 0x2 marks frames skipped by a failed atomic batch.
748748
let fr = FrameReceipt {
749749
status: FRAME_RECEIPT_STATUS_SKIPPED,
750750
gas_used: 0,
751751
logs: vec![],
752752
};
753753
let encoded = fr.encode_to_vec();
754+
// Consensus bytes: list(3) = [status 0x02, gas_used 0 (0x80), logs [] (0xc0)].
755+
// Asserted literally so a status-code regression (e.g. back to 0x3) fails
756+
// here, not only at a higher layer.
757+
assert_eq!(
758+
encoded,
759+
vec![0xc3, 0x02, 0x80, 0xc0],
760+
"skipped frame receipt must encode status as the byte 0x02"
761+
);
754762
let decoded = FrameReceipt::decode(&encoded).unwrap();
755763
assert_eq!(fr, decoded);
756764
assert_eq!(decoded.status, FRAME_RECEIPT_STATUS_SKIPPED);
765+
assert_eq!(decoded.status, 2, "skipped status must decode back to 2");
757766
}
758767

759768
#[test]

crates/networking/rpc/types/receipt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub struct RpcReceipt {
3333
#[derive(Debug, Serialize, Deserialize)]
3434
#[serde(rename_all = "camelCase")]
3535
pub struct RpcFrameReceipt {
36-
/// EIP-8141 frame status code: 0 = failure, 1 = success, 3 = skipped
36+
/// EIP-8141 frame status code: 0 = failure, 1 = success, 2 = skipped
3737
/// (atomic-batch failure). Serialized as a hex-encoded byte.
3838
#[serde(with = "serde_utils::u8::hex_str")]
3939
pub status: u8,

crates/vm/levm/src/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ pub struct ExecutionReport {
257257
pub payer_address: Option<Address>,
258258
/// For frame transactions: per-frame results (status, gas_used, logs).
259259
/// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
260-
/// 3 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
260+
/// 2 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
261261
pub frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
262262
}
263263

crates/vm/levm/src/opcode_handlers/frame_tx.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ impl OpcodeHandler for OpFrameParamHandler {
423423
0x05 => {
424424
// status -- exceptional halt if current/future frame.
425425
// Returns the EIP-8141 status code: 0 = failure, 1 = success,
426-
// 3 = skipped (atomic-batch failure).
426+
// 2 = skipped (atomic-batch failure).
427427
if idx >= ctx.current_frame_index {
428428
return Err(ExceptionalHalt::InvalidOpcode.into());
429429
}

crates/vm/levm/src/vm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ pub struct FrameTxContext {
508508
pub payer_address: Option<Address>,
509509
/// Per-frame execution results (status, gas_used, logs).
510510
/// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
511-
/// 3 = skipped due to failed atomic batch).
511+
/// 2 = skipped due to failed atomic batch).
512512
pub frame_results: Vec<(u8, u64, Vec<Log>)>,
513513
/// Index of the currently executing frame
514514
pub current_frame_index: usize,

docs/eip-8141.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ Returns a 32-byte word for the requested frame-scoped parameter of
287287
| 0x02 | mode |
288288
| 0x03 | flags |
289289
| 0x04 | len(data) |
290-
| 0x05 | status (0 = failure, 1 = success, 3 = skipped) — exceptional halt for the current or a future frame |
290+
| 0x05 | status (0 = failure, 1 = success, 2 = skipped) — exceptional halt for the current or a future frame |
291291
| 0x06 | allowed_scope (`flags & 0x03`) |
292292
| 0x07 | atomic_batch flag (`(flags >> 2) & 1`, returns 0 or 1) |
293293
| 0x08 | value |
@@ -440,7 +440,7 @@ For each frame in order:
440440
6. **Execute**: `run_execution()`, or run the built-in default code when the resolved target has no code.
441441
7. **Handle result**:
442442
- Success → commit substate, collect logs
443-
- Failure → revert substate, charge full gas_limit; if inside an atomic batch, unroll the batch (roll back to its entry snapshot and mark remaining batch frames SKIPPED with status 0x3)
443+
- Failure → revert substate, charge full gas_limit; if inside an atomic batch, unroll the batch (roll back to its entry snapshot and mark remaining batch frames SKIPPED with status 0x2)
444444
8. **Store result**: `(status, gas_used, logs)` in `frame_results`.
445445
9. **VERIFY enforcement**: A reverted VERIFY frame sets `tx_invalid = true` and breaks. A VERIFY frame that succeeds without APPROVE is valid.
446446
10. **Clear transient storage** between frames.
@@ -476,7 +476,7 @@ frame_receipt = [status, gas_used, logs]
476476

477477
There is no top-level `succeeded`, no bloom, and no aggregated `logs`. On decode,
478478
`succeeded` is **derived**: true iff every frame's status is `0x1` (success).
479-
`status` codes: `0x0` failure, `0x1` success, `0x3` skipped (frame skipped by a
479+
`status` codes: `0x0` failure, `0x1` success, `0x2` skipped (frame skipped by a
480480
failed atomic batch). The `payer` is the address recorded by APPROVE payment
481481
(zero address ⇒ `None`).
482482

test/tests/levm/eip8141_tests.rs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
use bytes::Bytes;
1414
use ethrex_blockchain::vm::StoreVmDatabase;
1515
use ethrex_common::types::{
16-
Account, BlockHeader, Code, FRAME_RECEIPT_STATUS_SUCCESS, Fork, Frame, FrameMode,
17-
FrameTransaction, Transaction,
16+
Account, BlockHeader, Code, FRAME_RECEIPT_STATUS_SKIPPED, FRAME_RECEIPT_STATUS_SUCCESS, Fork,
17+
Frame, FrameMode, FrameTransaction, Transaction,
1818
};
1919
use ethrex_common::{Address, H256, U256, constants::EMPTY_TRIE_HASH};
2020
use ethrex_crypto::NativeCrypto;
@@ -557,6 +557,102 @@ fn frameparam_reads_frame_index_from_stack_top() {
557557
);
558558
}
559559

560+
/// FRAMEPARAM(param=0x05, frameIndex=2) -> status of frame[2], then SSTORE at slot 0.
561+
/// Bytecode: PUSH1 0x05 (param), PUSH1 0x02 (frameIndex - top), FRAMEPARAM (0xB3),
562+
/// PUSH1 0x00 (slot key), SSTORE (0x55), STOP (0x00).
563+
const FRAMEPARAM_READ_FRAME2_STATUS: &[u8] =
564+
&[0x60, 0x05, 0x60, 0x02, 0xB3, 0x60, 0x00, 0x55, 0x00];
565+
566+
#[test]
567+
fn frameparam_status_of_skipped_frame_is_two() {
568+
// EIP-8141: a frame skipped by a failed atomic batch reports status 0x2
569+
// (not 0x3). Layout:
570+
// frame0: VERIFY on the sender -> APPROVE(3) -> payer + sender_approved.
571+
// frame1: DEFAULT, atomic-batch flag, reverts -> unrolls the batch.
572+
// frame2: DEFAULT, atomic-batch flag -> SKIPPED.
573+
// frame3: DEFAULT, no flag -> batch terminator, also SKIPPED.
574+
// frame4: DEFAULT, no flag -> reads FRAMEPARAM(0x05, 2) and stores it.
575+
let reverter = Address::from_low_u64_be(0xD1);
576+
let stop_ct = Address::from_low_u64_be(0xD2);
577+
let reader = Address::from_low_u64_be(0xD3);
578+
let tx = frame_tx_with_frames(vec![
579+
verify_frame(FUNDED_SENDER),
580+
Frame {
581+
mode: u8::from(FrameMode::Default),
582+
flags: 0x04,
583+
target: Some(reverter),
584+
gas_limit: 60_000,
585+
value: U256::zero(),
586+
data: Bytes::new(),
587+
},
588+
Frame {
589+
mode: u8::from(FrameMode::Default),
590+
flags: 0x04,
591+
target: Some(stop_ct),
592+
gas_limit: 30_000,
593+
value: U256::zero(),
594+
data: Bytes::new(),
595+
},
596+
Frame {
597+
mode: u8::from(FrameMode::Default),
598+
flags: 0x00,
599+
target: Some(stop_ct),
600+
gas_limit: 30_000,
601+
value: U256::zero(),
602+
data: Bytes::new(),
603+
},
604+
Frame {
605+
mode: u8::from(FrameMode::Default),
606+
flags: 0x00,
607+
target: Some(reader),
608+
// EIP-8037 (active at Hegota): the new-slot SSTORE spills
609+
// STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte (~98k) into
610+
// the frame's regular gas, so the budget must cover it.
611+
gas_limit: 300_000,
612+
value: U256::zero(),
613+
data: Bytes::new(),
614+
},
615+
]);
616+
let accounts = [
617+
(
618+
FUNDED_SENDER,
619+
AUTO_SEED_SENDER_BALANCE,
620+
0,
621+
Bytes::from(APPROVE_BOTH_CODE.to_vec()),
622+
),
623+
(
624+
reverter,
625+
U256::zero(),
626+
0,
627+
Bytes::from(PURE_REVERT_CODE.to_vec()),
628+
),
629+
(stop_ct, U256::zero(), 0, Bytes::from(vec![0x00u8])), // STOP
630+
(
631+
reader,
632+
U256::zero(),
633+
0,
634+
Bytes::from(FRAMEPARAM_READ_FRAME2_STATUS.to_vec()),
635+
),
636+
];
637+
let (result, db) = run_frame_tx(&accounts, tx);
638+
let report = result.expect("a DEFAULT-frame batch revert must not invalidate the tx");
639+
640+
let frame_results = report
641+
.frame_results
642+
.expect("frame tx report must carry per-frame results");
643+
assert_eq!(
644+
frame_results[2].0, FRAME_RECEIPT_STATUS_SKIPPED,
645+
"frame[2] must be recorded as skipped"
646+
);
647+
648+
// FRAMEPARAM(0x05) must surface the same code, and it must be 2.
649+
assert_eq!(
650+
storage_slot(&db, reader, H256::zero()),
651+
U256::from(2u64),
652+
"FRAMEPARAM param 0x05 must return 2 for a frame skipped by a failed atomic batch"
653+
);
654+
}
655+
560656
// ==================== APPROVE scope-0 bypass ====================
561657

562658
#[test]

0 commit comments

Comments
 (0)