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
115 changes: 73 additions & 42 deletions riscv-guests/l2-execution/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,58 @@ pub fn build(b: *std.Build) void {
l2_execution_ssz_tests.root_module.addImport("l2_execution_ssz", l2_execution_ssz_mod);
test_step.dependOn(&b.addRunArtifact(l2_execution_ssz_tests).step);

// ── l2-execution guest logic (src/l2_execution.zig), native build ───────────────────────────────
// Built for the native target so `extended-vanilla` below can link the SAME Linea-layer logic the
// riscv64 guest ELF runs (reached there via `evm_execution_guest.zig`'s relative import inside
// `guestMain` — see the module-wiring comment near `l2_execution_ssz_guest_mod` above). Needs the
// full zesu import set (MPT, executor types/tx-decode, primitives, accelerators for ecrecover)
// plus the sibling `l2_execution_ssz` module.
// ── Shared signed-tx fixture builder (test/tx_fixtures.zig) ─────────────────────────────────────
// RLP encoders for legacy/EIP-1559/EIP-4844 wire shapes plus real secp256k1 signing, shared by
// every test root that needs a genuinely signed, sender-recoverable transaction rather than a
// byte literal. Defined here (not inside the lazy execution-spec-tests block below): neither
// this module nor secp256k1_wrapper_mod depends on that lazy dependency, and l2_execution_tests
// (just below) is the first consumer needing them that lives outside that block.
const tx_fixtures_mod = b.createModule(.{
.root_source_file = b.path("test/tx_fixtures.zig"),
.target = native_target,
.optimize = host_optimize,
});
tx_fixtures_mod.addImport("zesu_executor", native_imports.executor);
tx_fixtures_mod.addImport("zesu_mpt", native_imports.mpt);

// secp256k1_wrapper.zig can't be rooted directly as its own module the way
// modexp_impl_mod/ripemd160_impl_mod are: unlike those two, this file is ALSO
// relatively-imported by zesu's own accel_impl root (already in this graph via
// accelerators), and Zig rejects one file belonging to two modules at once. The exposed
// accelerators surface has no path to `sign`/`getContext` either (it only exposes
// verify/ecrecover). A WriteFile step copies the file byte-for-byte to a fresh path
// nothing else claims, so the copy can root its own module. That module needs its own C
// include path for its `@cImport`'d secp256k1.h — C include paths are per-module and don't
// inherit from linkNativeZesuCrypto below (zesu's own build.zig hits the same constraint
// wiring accel_impl).
const secp256k1_wrapper_copy = b.addWriteFiles();
const secp256k1_wrapper_copy_path = secp256k1_wrapper_copy.addCopyFile(
zesu_native.path("src/crypto/backends/secp256k1_wrapper.zig"),
"secp256k1_wrapper.zig",
);
const secp256k1_wrapper_mod = b.createModule(.{
.root_source_file = secp256k1_wrapper_copy_path,
.target = native_target,
.optimize = host_optimize,
});
secp256k1_wrapper_mod.addIncludePath(.{ .cwd_relative = native_crypto.include_path });
tx_fixtures_mod.addImport("zesu_secp256k1", secp256k1_wrapper_mod);

// ── l2-execution guest logic (src/l2_execution.zig) unit tests ──────────────────────────────────
// These `zig build test` UNIT TESTS run only on the native target — like the l2_execution_ssz
// codec tests above, and like every other `b.addTest` artifact in this file. That's a standard
// Zig constraint, not specific to this module: a freestanding riscv64 target has no OS to run a
// `std.testing` binary against, so test binaries are always compiled and run for the native host.
// The `l2_execution.zig` LOGIC ITSELF is not native-only — it's compiled into the riscv64 guest
// ELF too, reached via `evm_execution_guest.zig`'s relative import inside `guestMain` (see the
// module-wiring comment near `l2_execution_ssz_guest_mod` above).
//
// These tests exercise the Linea-layer logic — the FTX rolling hash, dynamicChainConfigHash,
// hashAddressList/hashDigestList, the L1->L2 bridge storage-slot math, L2->L1 message
// extraction, forced-transaction dispatch, and the witness-backed MPT account/storage reads —
// against hand-built fixtures and Python-computed expected values (see Readme.md §6.3/§6.5/§2.1).
// Needs the full zesu import set (MPT, executor types/tx-decode, primitives, accelerators for
// ecrecover) plus the sibling `l2_execution_ssz` module.
const l2_execution_mod = b.createModule(.{
.root_source_file = b.path("src/l2_execution.zig"),
.target = native_target,
Expand All @@ -206,6 +252,25 @@ pub fn build(b: *std.Build) void {
addExecutionImports(l2_execution_mod, native_imports);
l2_execution_mod.addImport("l2_execution_ssz", l2_execution_ssz_mod);

const l2_execution_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("test/l2_execution_test.zig"),
.target = native_target,
.optimize = host_optimize,
}),
});
l2_execution_tests.root_module.addImport("l2_execution", l2_execution_mod);
l2_execution_tests.root_module.addImport("l2_execution_ssz", l2_execution_ssz_mod);
l2_execution_tests.root_module.addImport("zesu_executor", native_imports.executor);
l2_execution_tests.root_module.addImport("zesu_mpt", native_imports.mpt);
l2_execution_tests.root_module.addImport("zesu_input", native_imports.input);
l2_execution_tests.root_module.addImport("zesu_primitives", native_imports.primitives);
l2_execution_tests.root_module.addImport("zesu_allocator", native_imports.allocator);
l2_execution_tests.root_module.addImport("zesu_accelerators", native_imports.accelerators);
l2_execution_tests.root_module.addImport("tx_fixtures", tx_fixtures_mod);
linkNativeZesuCrypto(l2_execution_tests, native_target, native_crypto);
test_step.dependOn(&b.addRunArtifact(l2_execution_tests).step);

// ── l2-execution JSON output shape (test/l2_execution_json.zig) ─────────────────────────────────
// Native-only, pure std + the sibling `l2_execution_ssz` module (no zesu dependency): asserts
// `encodeOutputJson`'s field names/order/hex format agree byte-for-byte with the Python
Expand Down Expand Up @@ -352,16 +417,6 @@ pub fn build(b: *std.Build) void {

test_step.dependOn(&b.addRunArtifact(tests).step);

// ── Shared legacy-tx RLP encoder (test/legacy_tx_rlp.zig) ───────────────────────────────────
// One RLP encoder for a legacy transaction's fixed field list, shared by every test fixture
// that builds one from named fields rather than a byte literal.
const legacy_tx_rlp_mod = b.createModule(.{
.root_source_file = b.path("test/legacy_tx_rlp.zig"),
.target = native_target,
.optimize = host_optimize,
});
legacy_tx_rlp_mod.addImport("zesu_executor", native_imports.executor);

// ── Vanilla StatelessInput SSZ encoder (test/stateless_input_encode.zig) unit tests ────────
// Reuses the zesu_input/zesu_ssz_decode imports already resolved above for vanilla_wrap_mod,
// plus the fixtures module already built for the guest smoke test above.
Expand All @@ -376,7 +431,7 @@ pub fn build(b: *std.Build) void {
stateless_input_encode_tests.root_module.addImport("zesu_ssz_decode", native_imports.ssz_decode);
stateless_input_encode_tests.root_module.addImport("evm_execution_fixtures", fixtures_mod);
stateless_input_encode_tests.root_module.addImport("stateless_input_encode", stateless_input_encode_mod);
stateless_input_encode_tests.root_module.addImport("legacy_tx_rlp", legacy_tx_rlp_mod);
stateless_input_encode_tests.root_module.addImport("tx_fixtures", tx_fixtures_mod);
linkNativeZesuCrypto(stateless_input_encode_tests, native_target, native_crypto);
test_step.dependOn(&b.addRunArtifact(stateless_input_encode_tests).step);

Expand Down Expand Up @@ -407,29 +462,6 @@ pub fn build(b: *std.Build) void {
// ── Conflation-plan range scenario suite (test/l2_execution_range_test.zig) ───────────────
// Same relative-import reasoning as the parity test above: needs conflation_plan.zig's own
// import set wired directly here.
//
// secp256k1_wrapper.zig can't be rooted directly as its own module the way
// modexp_impl_mod/ripemd160_impl_mod are: unlike those two, this file is ALSO
// relatively-imported by zesu's own accel_impl root (already in this graph via
// accelerators), and Zig rejects one file belonging to two modules at once. The exposed
// accelerators surface has no path to `sign`/`getContext` either (it only exposes
// verify/ecrecover). A WriteFile step copies the file byte-for-byte to a fresh path
// nothing else claims, so the copy can root its own module. That module needs its own C
// include path for its `@cImport`'d secp256k1.h — C include paths are per-module and don't
// inherit from linkNativeZesuCrypto below (zesu's own build.zig hits the same constraint
// wiring accel_impl).
const secp256k1_wrapper_copy = b.addWriteFiles();
const secp256k1_wrapper_copy_path = secp256k1_wrapper_copy.addCopyFile(
zesu_native.path("src/crypto/backends/secp256k1_wrapper.zig"),
"secp256k1_wrapper.zig",
);
const secp256k1_wrapper_mod = b.createModule(.{
.root_source_file = secp256k1_wrapper_copy_path,
.target = native_target,
.optimize = host_optimize,
});
secp256k1_wrapper_mod.addIncludePath(.{ .cwd_relative = native_crypto.include_path });

const l2_execution_range_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("test/l2_execution_range_test.zig"),
Expand All @@ -444,9 +476,8 @@ pub fn build(b: *std.Build) void {
l2_execution_range_tests.root_module.addImport("zesu_input", native_imports.input);
l2_execution_range_tests.root_module.addImport("zesu_primitives", native_imports.primitives);
l2_execution_range_tests.root_module.addImport("zesu_rlp_decode", native_imports.rlp_decode);
l2_execution_range_tests.root_module.addImport("zesu_secp256k1", secp256k1_wrapper_mod);
l2_execution_range_tests.root_module.addImport("stateless_input_encode", stateless_input_encode_mod);
l2_execution_range_tests.root_module.addImport("legacy_tx_rlp", legacy_tx_rlp_mod);
l2_execution_range_tests.root_module.addImport("tx_fixtures", tx_fixtures_mod);
linkNativeZesuCrypto(l2_execution_range_tests, native_target, native_crypto);
test_step.dependOn(&b.addRunArtifact(l2_execution_range_tests).step);

Expand Down
15 changes: 12 additions & 3 deletions riscv-guests/l2-execution/src/l2_execution.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ const BRIDGE_L2L1_MESSAGE_SENT_TOPIC_0: [32]u8 = .{
0xbd, 0x80, 0xa1, 0xcf, 0x8d, 0xb7, 0x2e, 0x6c,
};

/// Storage layout of L2MessageService (see the Python reference implementation's docstring for provenance).
/// Storage layout of the L2MessageService contract: `lastAnchoredL1MessageNumber` at the fixed
/// slot below, `l1RollingHashes` (a mapping keyed by message number) at the mapping base slot
/// below. Solidity assigns storage slots by state-variable declaration order across the whole
/// inheritance chain — a property of the contract's compiled bytecode, independent of chain id or
/// deployment address, so the same slots hold on every deployment of the same contract version.
/// These two numbers are extracted from the compiled storage layout of
/// contracts/src/messaging/l2/L2MessageService.sol; if that layout ever changes (including a
/// `__gap` slot in an ancestor), they must be re-extracted, or these reads return wrong values and
/// the L1 finalization check fails.
const LAST_ANCHORED_L1_MESSAGE_NUMBER_SLOT: u64 = 280;
const L1_ROLLING_HASHES_MAPPING_BASE_SLOT: u64 = 281;

Expand Down Expand Up @@ -124,7 +132,7 @@ fn hashAddressList(alloc: std.mem.Allocator, values: []const [20]u8) ![32]u8 {

// ─── Witness-backed MPT state reads (mirrors state_transition.py's L2State) ───────────────────────
//
// Semantics (must match the Python reference implementation exactly — see Readme.md's state_transition.py docstrings):
// Semantics must match Readme.md's specification exactly (state_transition.py is its reference implementation):
// - account/slot proven absent from the trie -> `null` / `0` (NOT an error);
// - a witness node needed to resolve the path is missing from the pool -> `error.InvalidProof`
// propagates (guest rejection). `verifyAccountIndexed`/`verifyStorageIndexed` already draw this
Expand All @@ -133,7 +141,8 @@ fn hashAddressList(alloc: std.mem.Allocator, values: []const [20]u8) ![32]u8 {
// This is DELIBERATELY NOT `zesu_db.WitnessDatabase`: its `basic()`/`storage()` catch
// `error.InvalidProof` and silently treat it as absence (a leniency WitnessDatabase needs for
// precompile addresses that have no witness proof during live EVM execution) — that would mask a
// genuinely incomplete witness here, where the Python spec's `_mpt_lookup` raises instead.
// genuinely incomplete witness here, where the Python reference implementation's `_mpt_lookup`
// raises instead.

/// Account at `address` proven against `state_root`, or `null` if proven absent.
fn readAccount(state_root: [32]u8, address: [20]u8, node_index: *const mpt.NodeIndex) !?mpt.AccountState {
Expand Down
Loading
Loading