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
15 changes: 12 additions & 3 deletions crates/common/types/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use crate::types::TxType;
pub type Index = u64;

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

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

#[test]
fn test_frame_receipt_skipped_status_rlp_roundtrip() {
// Spec line 137: status code 0x3 marks frames skipped by a failed atomic batch.
// Spec line 137: status code 0x2 marks frames skipped by a failed atomic batch.
let fr = FrameReceipt {
status: FRAME_RECEIPT_STATUS_SKIPPED,
gas_used: 0,
logs: vec![],
};
let encoded = fr.encode_to_vec();
// Consensus bytes: list(3) = [status 0x02, gas_used 0 (0x80), logs [] (0xc0)].
// Asserted literally so a status-code regression (e.g. back to 0x3) fails
// here, not only at a higher layer.
assert_eq!(
encoded,
vec![0xc3, 0x02, 0x80, 0xc0],
"skipped frame receipt must encode status as the byte 0x02"
);
let decoded = FrameReceipt::decode(&encoded).unwrap();
assert_eq!(fr, decoded);
assert_eq!(decoded.status, FRAME_RECEIPT_STATUS_SKIPPED);
assert_eq!(decoded.status, 2, "skipped status must decode back to 2");
}

#[test]
Expand Down
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
2 changes: 1 addition & 1 deletion crates/vm/levm/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ pub struct ExecutionReport {
pub payer_address: Option<Address>,
/// For frame transactions: per-frame results (status, gas_used, logs).
/// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
/// 3 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
/// 2 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
pub frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
}

Expand Down
2 changes: 1 addition & 1 deletion crates/vm/levm/src/opcode_handlers/frame_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ impl OpcodeHandler for OpFrameParamHandler {
0x05 => {
// status -- exceptional halt if current/future frame.
// Returns the EIP-8141 status code: 0 = failure, 1 = success,
// 3 = skipped (atomic-batch failure).
// 2 = skipped (atomic-batch failure).
if idx >= ctx.current_frame_index {
return Err(ExceptionalHalt::InvalidOpcode.into());
}
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/levm/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ pub struct FrameTxContext {
pub payer_address: Option<Address>,
/// Per-frame execution results (status, gas_used, logs).
/// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
/// 3 = skipped due to failed atomic batch).
/// 2 = skipped due to failed atomic batch).
pub frame_results: Vec<(u8, u64, Vec<Log>)>,
/// Index of the currently executing frame
pub current_frame_index: usize,
Expand Down
6 changes: 3 additions & 3 deletions docs/eip-8141.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ Returns a 32-byte word for the requested frame-scoped parameter of
| 0x02 | mode |
| 0x03 | flags |
| 0x04 | len(data) |
| 0x05 | status (0 = failure, 1 = success, 3 = skipped) — exceptional halt for the current or a future frame |
| 0x05 | status (0 = failure, 1 = success, 2 = skipped) — exceptional halt for the current or a future frame |
| 0x06 | allowed_scope (`flags & 0x03`) |
| 0x07 | atomic_batch flag (`(flags >> 2) & 1`, returns 0 or 1) |
| 0x08 | value |
Expand Down Expand Up @@ -440,7 +440,7 @@ For each frame in order:
6. **Execute**: `run_execution()`, or run the built-in default code when the resolved target has no code.
7. **Handle result**:
- Success → commit substate, collect logs
- 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)
- 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)
8. **Store result**: `(status, gas_used, logs)` in `frame_results`.
9. **VERIFY enforcement**: A reverted VERIFY frame sets `tx_invalid = true` and breaks. A VERIFY frame that succeeds without APPROVE is valid.
10. **Clear transient storage** between frames.
Expand Down Expand Up @@ -476,7 +476,7 @@ frame_receipt = [status, gas_used, logs]

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

Expand Down
100 changes: 98 additions & 2 deletions test/tests/levm/eip8141_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
use bytes::Bytes;
use ethrex_blockchain::vm::StoreVmDatabase;
use ethrex_common::types::{
Account, BlockHeader, Code, FRAME_RECEIPT_STATUS_SUCCESS, Fork, Frame, FrameMode,
FrameTransaction, Transaction,
Account, BlockHeader, Code, FRAME_RECEIPT_STATUS_SKIPPED, FRAME_RECEIPT_STATUS_SUCCESS, Fork,
Frame, FrameMode, FrameTransaction, Transaction,
};
use ethrex_common::{Address, H256, U256, constants::EMPTY_TRIE_HASH};
use ethrex_crypto::NativeCrypto;
Expand Down Expand Up @@ -557,6 +557,102 @@ fn frameparam_reads_frame_index_from_stack_top() {
);
}

/// FRAMEPARAM(param=0x05, frameIndex=2) -> status of frame[2], then SSTORE at slot 0.
/// Bytecode: PUSH1 0x05 (param), PUSH1 0x02 (frameIndex - top), FRAMEPARAM (0xB3),
/// PUSH1 0x00 (slot key), SSTORE (0x55), STOP (0x00).
const FRAMEPARAM_READ_FRAME2_STATUS: &[u8] =
&[0x60, 0x05, 0x60, 0x02, 0xB3, 0x60, 0x00, 0x55, 0x00];

#[test]
fn frameparam_status_of_skipped_frame_is_two() {
// EIP-8141: a frame skipped by a failed atomic batch reports status 0x2
// (not 0x3). Layout:
// frame0: VERIFY on the sender -> APPROVE(3) -> payer + sender_approved.
// frame1: DEFAULT, atomic-batch flag, reverts -> unrolls the batch.
// frame2: DEFAULT, atomic-batch flag -> SKIPPED.
// frame3: DEFAULT, no flag -> batch terminator, also SKIPPED.
// frame4: DEFAULT, no flag -> reads FRAMEPARAM(0x05, 2) and stores it.
let reverter = Address::from_low_u64_be(0xD1);
let stop_ct = Address::from_low_u64_be(0xD2);
let reader = Address::from_low_u64_be(0xD3);
let tx = frame_tx_with_frames(vec![
verify_frame(FUNDED_SENDER),
Frame {
mode: u8::from(FrameMode::Default),
flags: 0x04,
target: Some(reverter),
gas_limit: 60_000,
value: U256::zero(),
data: Bytes::new(),
},
Frame {
mode: u8::from(FrameMode::Default),
flags: 0x04,
target: Some(stop_ct),
gas_limit: 30_000,
value: U256::zero(),
data: Bytes::new(),
},
Frame {
mode: u8::from(FrameMode::Default),
flags: 0x00,
target: Some(stop_ct),
gas_limit: 30_000,
value: U256::zero(),
data: Bytes::new(),
},
Frame {
mode: u8::from(FrameMode::Default),
flags: 0x00,
target: Some(reader),
// EIP-8037 (active at Hegota): the new-slot SSTORE spills
// STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte (~98k) into
// the frame's regular gas, so the budget must cover it.
gas_limit: 300_000,
value: U256::zero(),
data: Bytes::new(),
},
]);
let accounts = [
(
FUNDED_SENDER,
AUTO_SEED_SENDER_BALANCE,
0,
Bytes::from(APPROVE_BOTH_CODE.to_vec()),
),
(
reverter,
U256::zero(),
0,
Bytes::from(PURE_REVERT_CODE.to_vec()),
),
(stop_ct, U256::zero(), 0, Bytes::from(vec![0x00u8])), // STOP
(
reader,
U256::zero(),
0,
Bytes::from(FRAMEPARAM_READ_FRAME2_STATUS.to_vec()),
),
];
let (result, db) = run_frame_tx(&accounts, tx);
let report = result.expect("a DEFAULT-frame batch revert must not invalidate the tx");

let frame_results = report
.frame_results
.expect("frame tx report must carry per-frame results");
assert_eq!(
frame_results[2].0, FRAME_RECEIPT_STATUS_SKIPPED,
"frame[2] must be recorded as skipped"
);

// FRAMEPARAM(0x05) must surface the same code, and it must be 2.
assert_eq!(
storage_slot(&db, reader, H256::zero()),
U256::from(2u64),
"FRAMEPARAM param 0x05 must return 2 for a frame skipped by a failed atomic batch"
);
}

// ==================== APPROVE scope-0 bypass ====================

#[test]
Expand Down
Loading