10000 feat: Add EVM execution frame by roninjin10 · Pull Request #1665 · evmts/tevm-monorepo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: Add EVM execution frame #1665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 15, 2025
Merged

feat: Add EVM execution frame #1665

merged 10 commits into from
May 15, 2025

Conversation

roninjin10
Copy link
Collaborator
@roninjin10 roninjin10 commented May 15, 2025

Description

Implemented the EVM execution frame system, which is a core component of the Ethereum Virtual Machine. This PR adds:

  1. A new Frame module that represents a single execution context in the EVM
  2. Memory and stack implementations for the EVM
  3. State management interfaces for handling blockchain state
  4. Updated the EVM implementation to use the new frame system
  5. Added dedicated test modules for frame and EVM execution
  6. Implemented a logging system for better debugging

The implementation supports both contract calls and contract creation, with proper handling of execution results and gas accounting.

Testing

  • Added comprehensive unit tests for the frame system
  • Created separate test modules for frame and EVM functionality
  • Added dedicated test steps in the build system for testing specific components
  • Implemented debug logging to verify execution flow

Additional Information

Your ENS/address:

Summary by CodeRabbit

  • New Features

    • Introduced a modular EVM execution framework with support for execution frames, stack and memory management, and detailed logging.
    • Added new standalone tests for frame, EVM call, and contract creation execution.
    • Centralized logging configuration with customizable log levels and formatted output.
    • Refined the main application flow to initialize the EVM with state management and execute detailed call inputs.
  • Bug Fixes

    • Improved error handling for stack overflows, underflows, and invalid opcodes during EVM execution.
  • Tests

    • Comprehensive unit tests for frame lifecycle, stack/memory operations, and basic EVM call/create flows.
  • Chores

    • Enhanced build process to support modular test targets and improved test granularity.
    • Updated module imports for consistency and absolute referencing.

Copy link
vercel bot commented May 15, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
node ✅ Ready (Inspect) Visit Preview May 15, 2025 8:56pm
tevm-monorepo-app ✅ Ready (Inspect) Visit Preview May 15, 2025 8:56pm
1 Skipped Deployment
Name Status Preview Updated (UTC)
tevm-monorepo-tevm ⬜️ Ignored (Inspect) May 15, 2025 8:56pm

8000
Copy link
changeset-bot bot commented May 15, 2025

⚠️ No Changeset found

Latest commit: 5d8e41e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor
coderabbitai bot commented May 15, 2025

"""

Walkthrough

The changes introduce a modular EVM execution framework in Zig by adding a new frame module for managing execution frames, stack, and memory, along with a logging configuration module. The EVM core is refactored to utilize the new frame and state manager abstractions, replacing the interpret method with an execute method. The build script is updated to support modular builds and targeted tests. Comprehensive unit tests are added for frames, stack, memory, and execution logic.

Changes

File(s) Change Summary
build.zig Modularized build process, created individual modules for core components, established inter-module imports, and added separate test targets for frame and EVM components with dedicated build steps.
src/Evm/evm.zig Refactored Evm struct to include a stateManager pointer, replaced interpret with execute method implementing frame-based execution, added detailed logging, and split tests into call and create execution scenarios.
src/Evm/frame.zig New module defining EVM execution frames, including input/output types, memory and stack management, execution state, minimal execution logic, and a stub state manager with checkpointing and code loading.
src/Evm/frame_test.zig New test suite covering frame initialization, simple execution, stack and memory operations, and basic EVM call/create flows with assertions and mock state managers.
src/Evm/log_config.zig New module providing centralized logging configuration with dynamic log level based on build mode and a custom log function formatting messages with timestamps and scope.
src/Block/block.zig Changed import of address module from relative path to direct module import.
src/Rlp/rlp.zig Changed import of hex module from relative path to package-style import.
src/Utils/utils.zig New utility module exporting hex and keccak256 submodules for centralized access.
src/root.zig Refactored main function to instantiate StateManager and Evm, construct detailed FrameInput, and execute using the new execute method. Simplified imports accordingly.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Suite
    participant EVM as Evm
    participant Frame as Frame
    participant State as StateManager

    Test->>EVM: init(allocator, stateManager)
    Test->>EVM: execute(input)
    EVM->>State: loadCode(address)
    EVM->>Frame: createFrame(input, code, depth)
    Frame->>State: checkpoint()
    EVM->>Frame: execute(stateManager)
    Frame-->>EVM: FrameResult
    EVM-->>Test: FrameResult
Loading

Poem

🐇 In fields of Zig where bytes do dance,
New frames arise to give code a chance.
Stacks grow tall and memory flows,
With logs that shine and tests that glow.
The EVM hops with modular grace,
A rabbit’s joy in this bright new space! 🌿✨
"""

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between ee68efb and 5d8e41e.

📒 Files selected for processing (6)
  • build.zig (3 hunks)
  • src/Block/block.zig (1 hunks)
  • src/Evm/evm.zig (2 hunks)
  • src/Rlp/rlp.zig (2 hunks)
  • src/Utils/utils.zig (1 hunks)
  • src/root.zig (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/Block/block.zig
  • src/Rlp/rlp.zig
  • src/Utils/utils.zig
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Nx Cloud Agents (6)
  • GitHub Check: Nx Cloud Agents (3)
  • GitHub Check: Nx Cloud Agents (5)
  • GitHub Check: Nx Cloud Agents (2)
  • GitHub Check: Nx Cloud - Main Job
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
src/root.zig (2)

3-4: Improved module imports.

Clean approach to importing whole modules rather than individual submodules, making imports more maintainable.


7-28: Well-structured EVM initialization and execution.

The main function has been nicely refactored to use the new frame-based execution model. The code is clear, with good comments, and follows a logical flow:

  1. Create state manager
  2. Initialize EVM
  3. Set up frame input
  4. Execute

This matches the PR's objective of implementing the EVM execution frame system.

build.zig (1)

24-87: Good modularization of the codebase.

The build system has been well-organized into individual modules with clear dependencies, making the codebase more maintainable and easier to navigate.

src/Evm/evm.zig (4)

2-8: Good use of scoped logging.

The addition of scoped logging for EVM operations will make debugging much easier and more focused.


108-115: Enhanced EVM with state management.

Good addition of the state manager to the EVM struct, allowing for state tracking during execution.


225-262: Good test coverage for call execution.

The test for call execution is well-structured and tests the happy path for executing a simple EVM call with a STOP opcode.


264-294: Good test coverage for create execution.

The test for create execution thoroughly covers the contract creation flow with a simple STOP opcode.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (7)
src/Evm/log_config.zig (2)

20-23: std.time.milliTimestamp() is called twice – minor performance & skew issue

You compute millis and seconds from two independent timestamp calls. In fast-moving loops that could yield inconsistent values (e.g., seconds incremented but millis from previous second).

-const millis = std.time.milliTimestamp() % 1000;
-const seconds = @divFloor(std.time.milliTimestamp(), 1000);
+const ts = std.time.milliTimestamp();
+const millis = ts % 1000;
+const seconds = @divFloor(ts, 1000);

26-35: Fixed-width padding {d:4}.{d:3} may truncate after ~9999 s

When long-running processes exceed ~2 h 46 m, the seconds component will overflow the 4-digit field and mis-align the log. Consider dynamic width or a larger field.

src/Evm/frame.zig (3)

107-114: Out-of-bounds load silently returns empty slice

Silently returning [] when offset >= len hides logic bugs and diverges from EVM
behaviour (which returns zeroes but still charges gas).
Consider returning a slice of zeroes with the requested length, or at least an error to make bugs visible in tests.


131-137: push appends after overflow check but does not cap length

The check self.data.items.len >= 1024 is correct, yet in case an external caller
manipulates data.items.len directly
the guarantee vanishes.
Prefer encapsulating the stack list (e.g. make data private and expose only push/pop)
or assert after append that len <= 1024.


138-143: Redundant nil-coalescing in pop

You already guard against len == 0; pop() will therefore never return null.
The orelse is unreachable:

-        return self.data.pop() orelse return error.StackUnderflow;
+        return self.data.pop();
src/Evm/frame_test.zig (2)

127-140: Memory zero-initialisation not asserted

Given the earlier note about Memory.store, adding a load from an untouched
offset (e.g. memory.load(10, 4)) and expecting four zero bytes would catch
regressions in zero-fill behaviour.


170-176: Test only checks status, not gas usage nor refunds

Verifying gasUsed and gasRefunded fields would strengthen the regression
suite once gas accounting is implemented.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 78da30b and ee68efb.

📒 Files selected for processing (5)
  • build.zig (2 hunks)
  • src/Evm/evm.zig (2 hunks)
  • src/Evm/frame.zig (1 hunks)
  • src/Evm/frame_test.zig (1 hunks)
  • src/Evm/log_config.zig (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: Nx Cloud Agents (4)
  • GitHub Check: Nx Cloud Agents (6)
  • GitHub Check: Nx Cloud Agents (5)
  • GitHub Check: Nx Cloud Agents (2)
  • GitHub Check: Nx Cloud Agents (3)
  • GitHub Check: Nx Cloud Agents (1)
🔇 Additional comments (3)
build.zig (1)

38-40: Verify that exe_mod and wasm_mod can resolve Address

zigevm_mod gets the Address import, but any code pulled in by main.zig or the WASM root that directly uses @import("Address") will fail to compile because exe_mod / wasm_mod do not know about it.
If those roots transitively depend on Address only through zigevm_mod, you’re fine; otherwise add:

exe_mod.addImport("Address", address_mod);
wasm_mod.addImport("Address", address_mod);
src/Evm/frame.zig (1)

299-301: gasUsed is always zero

state.gas.remaining is never decremented, so gasUsed is constant.
Even a placeholder interpreter should account for at least base cost of the executed
instruction to avoid misleading test coverage.

src/Evm/frame_test.zig (1)

102-104: Undeclared identifier u256 in test scope

u256 is used without an usingnamespace std; or explicit
const u256 = @import("std").meta.Int(.unsigned, 256);.
If another file does not already pub-export this alias, the test will fail
to compile.

build.zig Outdated
Comment on lines 128 to 147
const frame_test_step = b.step("test-frame", "Run EVM frame tests");
frame_test_step.dependOn(&run_frame_test.step);

// Add a test for evm.zig
const evm_test = b.addTest(.{
.name = "evm-test",
.root_source_file = b.path("src/Evm/evm.zig"),
.target = target,
.optimize = optimize,
});

// Add the Address module to evm_test
evm_test.root_module.addImport("Address", address_mod);

const run_evm_test = b.addRunArtifact(evm_test);

// Add a separate step for testing the EVM
const evm_test_step = b.step("test-evm", "Run EVM tests");
evm_test_step.dependOn(&run_evm_test.step);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displa 10000 yed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

New test steps are not wired into the umbrella test step

You correctly created test-frame and test-evm, but the default zig build test target (the test_step) does not depend on either. CI users running the canonical command will silently skip the new suites.

 test_step.dependOn(&run_lib_unit_tests.step);
 test_step.dependOn(&run_exe_unit_tests.step);
+test_step.dependOn(&frame_test_step.step);
+test_step.dependOn(&evm_test_step.step);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const frame_test_step = b.step("test-frame", "Run EVM frame tests");
frame_test_step.dependOn(&run_frame_test.step);
// Add a test for evm.zig
const evm_test = b.addTest(.{
.name = "evm-test",
.root_source_file = b.path("src/Evm/evm.zig"),
.target = target,
.optimize = optimize,
});
// Add the Address module to evm_test
evm_test.root_module.addImport("Address", address_mod);
const run_evm_test = b.addRunArtifact(evm_test);
// Add a separate step for testing the EVM
const evm_test_step = b.step("test-evm", "Run EVM tests");
evm_test_step.dependOn(&run_evm_test.step);
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
test_step.dependOn(&frame_test_step.step);
test_step.dependOn(&evm_test_step.step);
🤖 Prompt for AI Agents
In build.zig around lines 128 to 147, the new test steps "test-frame" and
"test-evm" are created but not linked to the main umbrella test step. To fix
this, add dependencies from the main test step to both frame_test_step and
evm_test_step so that running the default "zig build test" will include these
new test suites.

Comment on lines +170 to +183
if (input == .Create) {
if (frameResult == .Call) {
log.debug("Converting Call result to Create result", .{});
const callResult = frameResult.Call;
return frame.FrameResult{ .Create = .{
.status = callResult.status,
.returnData = callResult.returnData,
.gasUsed = callResult.gasUsed,
.gasRefunded = callResult.gasRefunded,
.createdAddress = null,
} };
}
} else if (input == .Call) {
if (frameResult == .Create) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Illegal union comparisons: input == .Create / frameResult == .Call

Both FrameInput and FrameResult variants carry payloads; direct equality with a tag literal is only allowed for tag-only unions. Use input == .Create only if .Create has no payload.

Correct pattern:

if (input.tag == .Create) { ... }

or switch on the tag:

switch (input) {
    .Create => |payload| { ... }
    .Call   => |payload| { ... }
}

Same issue exists for frameResult == .Call and callInput == .Call.

🤖 Prompt for AI Agents
In src/Evm/evm.zig around lines 170 to 183, the code incorrectly compares union
values directly to tagged literals like `input == .Create` and `frameResult ==
.Call`, which is invalid for unions with payloads. To fix this, replace these
comparisons with checks on the `.tag` field, such as `input.tag == .Create` and
`frameResult.tag == .Call`, or use a switch statement on the union to handle
each variant properly. This ensures correct and legal union variant checking.

Comment on lines +119 to +124
// Create checkpoint for state changes
const checkpoint = self.stateManager.checkpoint();

// Initialize and return the frame
return try frame.Frame.init(self.allocator, input, code, depth, checkpoint);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

StateManager.checkpoint is invoked but not defined

frame.StateManager in the provided context only contains three hash-maps. Calling .checkpoint() will fail at compile time unless an extension method exists elsewhere.

If checkpointing is still on the roadmap, stub it in StateManager:

pub fn checkpoint(self: *StateManager) Checkpoint {
    // todo: implement proper snapshotting
    return Checkpoint{};
}

and ensure Checkpoint is a concrete type accepted by Frame.init.

🤖 Prompt for AI Agents
In src/Evm/evm.zig around lines 119 to 124, the call to
self.stateManager.checkpoint() fails because the checkpoint method is not
defined in StateManager. To fix this, add a stub checkpoint method to the
StateManager struct that returns a concrete Checkpoint type, for example by
defining a pub fn checkpoint(self: *StateManager) Checkpoint that returns an
empty or default Checkpoint instance. Also ensure that the Checkpoint type is
properly defined and compatible with the Frame.init method's expected argument.

Comment on lines +247 to +249
// Mock code for testing
stateManager.mockCode = &[_]u8{0x00}; // STOP opcode

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

mockCode field not present on StateManager – test won’t compile

The unit test sets stateManager.mockCode = &[_]u8{0x00}; but StateManager does not declare such a member.
Quickest path: extend the struct with a mockCode slice guarded by std.testing or provide a real loadCode implementation that returns hard-coded STOP when empty.

🤖 Prompt for AI Agen E864 ts
In src/Evm/evm.zig around lines 247 to 249, the StateManager struct lacks a
mockCode field, causing the test to fail compilation. To fix this, add a
mockCode slice field to the StateManager struct, but only include it when
compiling tests by guarding it with std.testing. Alternatively, implement the
loadCode method to return a hard-coded STOP opcode when no code is present,
removing the need for mockCode.

Comment on lines +130 to +140
var code: frame.Bytes = undefined;

// Get the code based on frame input type
switch (input) {
.Call => |call| {
// For a call, load the code from the target address
log.debug("Loading code for address: {any}", .{call.codeAddress});
code = try self.stateManager.loadCode(call.codeAddress);
log.debug("Call input - Gas Limit: {d}, Code length: {d}", .{ call.gasLimit, code.len });
},
.Create => |create| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

loadCode missing on StateManager

self.stateManager.loadCode(call.codeAddress) assumes a helper that does not exist in the declared struct. Either implement:

pub fn loadCode(self: *StateManager, addr: address.Address) !frame.Bytes { ... }

or move code retrieval logic elsewhere.

🤖 Prompt for AI Agents
In src/Evm/evm.zig around lines 130 to 140, the call to
self.stateManager.loadCode does not compile because the loadCode method is
missing from the StateManager struct. To fix this, implement a loadCode method
on StateManager with the signature pub fn loadCode(self: *StateManager, addr:
address.Address) !frame.Bytes that retrieves and returns the code bytes for the
given address. Alternatively, if code retrieval belongs elsewhere, move this
logic out of StateManager and adjust the call accordingly.

Comment on lines +198 to +220
// In a real implementation, we would recursively handle the call
// but for now we'll just return a success result
log.debug("Got Call result", .{});
if (callInput == .Call) {
log.debug("Call type: Call", .{});
return frame.FrameResult{ .Call = .{
.status = .Success,
.returnData = &[_]u8{},
.gasUsed = 0,
.gasRefunded = 0,
} };
} else {
log.debug("Call type: Create", .{});
return frame.FrameResult{ .Create = .{
.status = .Success,
.returnData = &[_]u8{},
.gasUsed = 0,
.gasRefunded = 0,
.createdAddress = null,
} };
}
},
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Logic inside .Call => |callInput| mis-uses the payload

Inside this branch callInput is already the payload (not the union). Comparing it to .Call or .Create is nonsensical.
Moreover the function currently fabricates a success result instead of performing recursion; consider returning result unchanged until full recursion is implemented, to avoid discarding gas accounting & returndata.

-.Call => |callInput| {
-    // placeholder logic
-    ...
-}
+else => |nested| {
+    // TODO: recursive execution of nested frames
+    return nested;
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Evm/evm.zig around lines 198 to 220, the code incorrectly compares
callInput, which is already the payload, to .Call or .Create, which is invalid.
Instead of fabricating a success result, modify the logic to return the existing
result unchanged to preserve gas accounting and return data until full recursion
is implemented.

Comment on lines +97 to +105
pub fn store(self: *Memory, offset: usize, value: []const u8) !void {
// Expand memory if needed
const requiredSize = offset + value.len;
if (requiredSize > self.data.items.len) {
try self.data.resize(requiredSize);
}

@memcpy(self.data.items[offset..offset+value.len], value);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Memory expansion leaves bytes un-initialised

ArrayList.resize does not zero-initialise the newly allocated region.
EVM memory, however, must be zero-filled when first expanded (see Yellow-Paper §9.4 Memory).
Reading from an uninitialised segment after a store at a sparse offset will therefore return
garbage instead of 0x00.

-        if (requiredSize > self.data.items.len) {
-            try self.data.resize(requiredSize);
-        }
+        if (requiredSize > self.data.items.len) {
+            const old_len = self.data.items.len;
+            try self.data.resize(requiredSize);
+            // Zero-fill the gap [old_len .. requiredSize)
+            @memset(self.data.items[old_len..requiredSize], 0);
+        }

(This uses @memset; alternatively std.mem.set(u8, slice, 0).)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn store(self: *Memory, offset: usize, value: []const u8) !void {
// Expand memory if needed
const requiredSize = offset + value.len;
if (requiredSize > self.data.items.len) {
try self.data.resize(requiredSize);
}
@memcpy(self.data.items[offset..offset+value.len], value);
}
pub fn store(self: *Memory, offset: usize, value: []const u8) !void {
// Expand memory if needed
const requiredSize = offset + value.len;
if (requiredSize > self.data.items.len) {
const old_len = self.data.items.len;
try self.data.resize(requiredSize);
// Zero-fill the gap [old_len .. requiredSize)
@memset(self.data.items[old_len..requiredSize], 0);
}
@memcpy(self.data.items[offset..offset+value.len], value);
}
🤖 Prompt for AI Agents
In src/Evm/frame.zig around lines 97 to 105, the memory expansion in the store
function does not zero-initialize the newly allocated bytes, which violates
EVM's requirement for zero-filled memory on expansion. After resizing the data
array, explicitly zero out the newly added region using @memset or std.mem.set
to ensure all new bytes are initialized to 0 before copying the value.

Comment on lines +268 to +318
// Simple implementation for initial tests
// Just check for basic opcodes and handle them
log.debug("Starting execution", .{});

if (self.code.len == 0) {
log.debug("Empty code, returning success", .{});
// Empty code returns success
return FrameOrCall{
.Result = FrameResult{
.Call = .{
.status = .Success,
.returnData = &[_]u8{},
.gasUsed = self.input.getGasLimit() - self.state.gas.remaining,
.gasRefunded = self.state.gas.refunded,
}
}
};
}

log.debug("Code length is {d}, first byte: {x:0>2}", .{self.code.len, self.code[0]});

// In a real implementation, we would loop through the bytecode and interpret opcodes
// but for this simplified version, we'll just check for STOP (0x00)
if (self.code[0] == 0x00) {
log.debug("Found STOP opcode, returning success", .{});
// STOP opcode
return FrameOrCall{
.Result = FrameResult{
.Call = .{
.status = .Success,
.returnData = &[_]u8{},
.gasUsed = self.input.getGasLimit() - self.state.gas.remaining,
.gasRefunded = self.state.gas.refunded,
}
}
};
}

log.debug("Unknown opcode, returning InvalidOpcode", .{});
// Default response for other opcodes (not yet implemented)
return FrameOrCall{
.Result = FrameResult{
.Call = .{
.status = .InvalidOpcode,
.returnData = &[_]u8{},
.gasUsed = self.input.getGasLimit() - self.state.gas.remaining,
.gasRefunded = self.state.gas.refunded,
}
}
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

execute always tags the result as .Call

For FrameInput.Create you still return a .Call result, which makes the caller
mis-interpret the outcome and loses the createdAddress field.

-        return FrameOrCall{ 
-            .Result = FrameResult{
-                .Call = .{
+        const tag: FrameResult = switch (self.input) {
+            .Create => .{
+                .Create = .{
+                    .status = .Success,
+                    .returnData = &[_]u8{},
+                    .gasUsed = self.input.getGasLimit() - self.state.gas.remaining,
+                    .gasRefunded = self.state.gas.refunded,
+                    .createdAddress = null, // TODO: compute address
+                },
+            },
+            else => .{
+                .Call = .{
                     .status = .Success,
                     .returnData = &[_]u8{},
                     .gasUsed = self.input.getGasLimit() - self.state.gas.remaining,
                     .gasRefunded = self.state.gas.refunded,
                 }
-            }
-        };
+            },
+        };
+
+        return .{ .Result = tag };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Evm/frame.zig between lines 268 and 318, the execute function always
returns a result tagged as .Call, even when the FrameInput is of type Create.
This causes the caller to misinterpret the outcome and lose the createdAddress
field. To fix this, modify the return statements to check if the input is a
Create operation and return a FrameResult tagged as .Create with the appropriate
createdAddress field set; otherwise, return .Call as before. Ensure that the
result type matches the input type to preserve all relevant data.

Comment on lines +98 to +116
// Push and pop values
try stack.push(123);
try stack.push(456);

try testing.expectEqual(@as(u256, 456), try stack.pop());
try testing.expectEqual(@as(u256, 123), try stack.pop());

// Test stack underflow
try testing.expectError(error.StackUnderflow, stack.pop());

// Push many values to test overflow
var i: usize = 0;
while (i < 1024) : (i += 1) {
try stack.push(i);
}

// Test stack overflow
try testing.expectError(error.StackOverflow, stack.push(1025));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

usizeu256 conversion required in loop

i is usize; implicit widening to u256 is disallowed for run-time values, causing a
compile error.

-        try stack.push(i);
+        try stack.push(@as(u256, i));

Alternatively, declare var i: u256 = 0.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Push and pop values
try stack.push(123);
try stack.push(456);
try testing.expectEqual(@as(u256, 456), try stack.pop());
try testing.expectEqual(@as(u256, 123), try stack.pop());
// Test stack underflow
try testing.expectError(error.StackUnderflow, stack.pop());
// Push many values to test overflow
var i: usize = 0;
while (i < 1024) : (i += 1) {
try stack.push(i);
}
// Test stack overflow
try testing.expectError(error.StackOverflow, stack.push(1025));
}
// Push many values to test overflow
var i: usize = 0;
while (i < 1024) : (i += 1) {
try stack.push(@as(u256, i));
}
// Test stack overflow
try testing.expectError(error.StackOverflow, stack.push(1025));
}
🤖 Prompt for AI Agents
In src/Evm/frame_test.zig around lines 98 to 116, the loop variable i is
declared as usize but is used where a u256 is expected, causing a compile error
due to disallowed implicit widening. Fix this by changing the declaration of i
from usize to u256 to match the expected type and avoid the conversion error.

@vercel vercel bot temporarily deployed to Preview – node May 15, 2025 20:49 Inactive
@vercel vercel bot temporarily deployed to Preview – node May 15, 2025 20:50 Inactive
@roninjin10 roninjin10 changed the title feat: Add abi encoding/decoding feat: Add EVM execution frame May 15, 2025
@roninjin10 roninjin10 merged commit d60f0e2 into main May 15, 2025
15 checks passed
@roninjin10 roninjin10 deleted the evm-frame branch May 15, 2025 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant
0