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
28 changes: 20 additions & 8 deletions riscv-guests/l2-execution/src/evm_execution_guest.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub const execution = @import("execution.zig");
const l2_execution = @import("l2_execution.zig");
const l2_execution_ssz = @import("l2_execution_ssz");

/// Exit-code taxonomy for guest failures (Readme.md §2.5); `pub` for the guest's test suite, like
/// `execution` above.
pub const guest_errors = @import("guest_errors.zig");

// Heap starts at the address defined by the linker script (canonical Lineth layout: `_heap_start` = 0x48800000, grows up).
extern var _heap_start: u8;
// Linker script does not actually constraint the heap to 256 MiB, but this is a reasonable upper bound
Expand All @@ -25,9 +29,11 @@ const GUEST_HEAP_SIZE: usize = 256 * 1024 * 1024;
// C-backed crypto instead.

/// zkVM guest entry. Reads the extended `L2ExecutionProofPrivateInput` via `read_input`, runs
/// `l2_execution.runL2Execution`, and emits the SSZ output via `write_output`. Exits 0 on success,
/// 1 on any error. `read_input`/`write_output` are satisfied by zesu-zkvm's `linea_zkvm_io` — where
/// the input lives and how the output surfaces is the proving system's concern, not the guest's.
/// `l2_execution.runL2Execution`, and emits the SSZ output via `write_output`. Exits 0 on success;
/// on failure, exits with `guest_errors.exitCode(err)` — a deterministic, category-stable nonzero
/// code per Readme.md §2.5 — after logging the failing error's name via `zkvm_log`.
/// `read_input`/`write_output` are satisfied by zesu-zkvm's `linea_zkvm_io` — where the input lives
/// and how the output surfaces is the proving system's concern, not the guest's.
///
/// This frozen riscv64 binary has no argv, so output format is fixed at build time (always SSZ);
/// the `--json`/`--ssz` toggle lives on the native `l2-execution-runner` tool instead.
Expand All @@ -43,15 +49,21 @@ fn guestMain() callconv(.c) noreturn {
zkvm_io.read_input(&buf_ptr, &buf_size);
const raw_input = buf_ptr[0..buf_size];

const out = runL2ExecutionGuest(allocator, raw_input) catch exit(1);
const out = runL2ExecutionGuest(allocator, raw_input) catch |err| {
// `zkvm_log` is the guest's one diagnostic sink (a documented no-op until the prover
// exposes logging that doesn't alias the output commitment).
const name = @errorName(err);
zkvm_log(0, name.ptr, name.len);
exit(guest_errors.exitCode(err));
};
zkvm_io.write_output(&out);
exit(0);
}

/// Decode -> execute -> encode, factored out of `guestMain` so the whole pipeline is one
/// `catch exit(1)` away from a clean guest rejection. Returns the output BY VALUE (a small,
/// fixed-size array — see `l2_execution_ssz.encodeOutput`'s doc comment) rather than an
/// allocator-backed slice: there's nothing for an allocator to do here.
/// Decode -> execute -> encode, factored out of `guestMain` so the whole pipeline is one `catch`
/// away from a clean, categorized guest rejection (`guest_errors.exitCode`, Readme.md §2.5).
/// Returns the output BY VALUE (a small, fixed-size array — see `l2_execution_ssz.encodeOutput`'s
/// doc comment) rather than an allocator-backed slice: there's nothing for an allocator to do here.
fn runL2ExecutionGuest(allocator: std.mem.Allocator, raw_input: []const u8) ![l2_execution_ssz.OUTPUT_SIZE]u8 {
const decoded = try l2_execution_ssz.decodeInput(allocator, raw_input);
const result = try l2_execution.runL2Execution(allocator, decoded);
Expand Down
109 changes: 109 additions & 0 deletions riscv-guests/l2-execution/src/guest_errors.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//! Deterministic guest exit-code taxonomy (Readme.md §2.5 "Guest Termination Semantics").
//!
//! Failures map to coarse, category-stable exit codes: adding or renaming an individual error never
//! renumbers a category, so codes stay meaningful to operators across guest versions. Success exits
//! 0; every category here is nonzero, per the standard's failed-termination requirement.
//!
//! Standalone by design: Zig error literals are global (matched by name program-wide), so this file
//! needs no imports.
//!
//! Adding a new Linea-layer error means adding it to BOTH `linea_errors` and `exitCode`'s switch in
//! the same change — the comptime guard in the guest's test suite fails on any `linea_errors`
//! member that `exitCode` leaves at CODE_UNKNOWN.

/// Matches the width of the guest's exit primitive.
pub const ExitCode = u64;

/// Fallback for engine/EVM/zesu-internal errors and anything not yet triaged — an unmapped error
/// still fails the guest with a nonzero exit.
pub const CODE_UNKNOWN: ExitCode = 1;
pub const CODE_INVALID_SSZ_ENVELOPE: ExitCode = 2;
pub const CODE_INVALID_STATELESS_INPUT: ExitCode = 3;
pub const CODE_CONFLATION_INVARIANT: ExitCode = 4;
pub const CODE_POLICY_REJECT: ExitCode = 5;
pub const CODE_FORCED_TX_VIOLATION: ExitCode = 6;
/// A node needed to resolve an MPT proof path was missing from the witness pool (a proof of ABSENCE
/// resolves to `null`/`0` instead, never here), at whichever layer surfaces the read: a direct
/// Linea-layer MPT read, the witness header chain, or the EVM's witness-backed database during
/// delegated per-block execution.
pub const CODE_WITNESS_RESOLUTION: ExitCode = 7;
Comment on lines +25 to +29

/// Every error a Linea-layer function deliberately returns, in `exitCode`'s arm order.
pub const linea_errors = error{
InvalidSsz,

InvalidStatelessInput,

EmptyPayloads,
ChainIdMismatch,
ParentHashChainMismatch,
BaseFeeNotConstant,
FeeRecipientMismatch,
MissingParentHeaderWitness,
InvalidGenesisParentHash,

ExecutionRequestsNotSupported,
WithdrawalsNotSupported,
UnsupportedFork,

ForcedTxOutOfOrder,
ForcedTxDeadlineExceeded,
ForcedTxSenderRecoveryFailed,
UnknownForcedTxAcceptance,
IncludedForcedTxNotInBlock,
InvalidForcedTxFoundInBlock,
BadNonceMismatch,
BadBalanceMismatch,
FilteredAddressToOnContractCreation,
ForcedTxSenderAbsent,

RollingHashNumberOverflow,
RollingHashNumberDecreased,
InvalidBridgeMessageLog,
InvalidProof,
InvalidWitness,
};

/// Maps a guest failure to its deterministic, category-stable exit code (Readme.md §2.5).
pub fn exitCode(err: anyerror) ExitCode {
return switch (err) {
error.InvalidSsz => CODE_INVALID_SSZ_ENVELOPE,

error.InvalidStatelessInput => CODE_INVALID_STATELESS_INPUT,

error.EmptyPayloads,
error.ChainIdMismatch,
error.ParentHashChainMismatch,
error.BaseFeeNotConstant,
error.FeeRecipientMismatch,
error.MissingParentHeaderWitness,
error.InvalidGenesisParentHash,
=> CODE_CONFLATION_INVARIANT,

error.ExecutionRequestsNotSupported,
error.WithdrawalsNotSupported,
error.UnsupportedFork,
=> CODE_POLICY_REJECT,

error.ForcedTxOutOfOrder,
error.ForcedTxDeadlineExceeded,
error.ForcedTxSenderRecoveryFailed,
error.UnknownForcedTxAcceptance,
error.IncludedForcedTxNotInBlock,
error.InvalidForcedTxFoundInBlock,
error.BadNonceMismatch,
error.BadBalanceMismatch,
error.FilteredAddressToOnContractCreation,
error.ForcedTxSenderAbsent,
=> CODE_FORCED_TX_VIOLATION,

error.RollingHashNumberOverflow,
error.RollingHashNumberDecreased,
error.InvalidBridgeMessageLog,
error.InvalidProof,
error.InvalidWitness,
=> CODE_WITNESS_RESOLUTION,

else => CODE_UNKNOWN,
};
}
26 changes: 26 additions & 0 deletions riscv-guests/l2-execution/test/evm_execution_guest_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,29 @@ test "executeStatelessInputWithLogs matches vanilla executeStatelessInput's root
try std.testing.expectEqual(@as(usize, 0), receipt.logs.len);
}
}

test "guest_errors.exitCode pins one representative code per category" {
const guest_errors = guest.guest_errors;

// The raw numbers are the point: these codes are load-bearing for operators, so renumbering
// must break this test. `error.OutOfMemory` stands in for any error outside `linea_errors`.
try std.testing.expectEqual(@as(guest_errors.ExitCode, 1), guest_errors.exitCode(error.OutOfMemory));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 2), guest_errors.exitCode(error.InvalidSsz));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 3), guest_errors.exitCode(error.InvalidStatelessInput));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 4), guest_errors.exitCode(error.EmptyPayloads));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 5), guest_errors.exitCode(error.UnsupportedFork));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 6), guest_errors.exitCode(error.BadNonceMismatch));
try std.testing.expectEqual(@as(guest_errors.ExitCode, 7), guest_errors.exitCode(error.InvalidProof));
}

test "guest_errors.linea_errors: every listed error maps to a non-unknown category" {
const guest_errors = guest.guest_errors;
comptime {
for (@typeInfo(guest_errors.linea_errors).error_set.?) |e| {
const err: anyerror = @field(guest_errors.linea_errors, e.name);
if (guest_errors.exitCode(err) == guest_errors.CODE_UNKNOWN) {
@compileError("guest_errors.linea_errors: '" ++ e.name ++ "' maps to CODE_UNKNOWN — add an explicit exitCode() category arm");
}
}
}
}
6 changes: 3 additions & 3 deletions rollup_spec/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,11 +754,11 @@ These propagate through the proof tree symmetrically to the L1→L2 bridge field

The guest processes FTXs in ascending `ftxNumber` order after completing normal block execution. For each FTX in the range:

**Deadline constraint.** Assert:
**Deadline constraint.** For a FTX handled in the block with number `handlingBlockNumber`, assert:
```
ftx.deadlineBlockNumber >= prevLastBlockNumber
ftx.deadlineBlockNumber >= handlingBlockNumber
```
A FTX whose deadline falls before the start of this range was already expired; it must have been handled in a prior range. If it wasn't, finalization of the prior range would have been blocked.
Handling a FTX after its deadline would break the forced-inclusion promise, so the declared outcome must land in a block at or before the deadline. This also subsumes range-level expiry: a FTX whose deadline falls before the start of this range was already expired and must have been handled in a prior range — otherwise finalization of the prior range would have been blocked.

**Authenticity.** Re-derive the rolling hash step and assert it matches the L1-stored value:
```
Expand Down
Loading