8000 ✨ feat(evm): implement production-quality Frame module for execution context management by roninjin10 · Pull Request #1736 · evmts/tevm-monorepo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

✨ feat(evm): implement production-quality Frame module for execution context management #1736

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 1 commit into from
May 28, 2025

Conversation

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

Description

Added a new Frame module to the EVM implementation, which manages execution context for a single call frame. The Frame module handles stack, memory, program counter, gas tracking, and return data management with zero-allocation paths for common operations.

Added comprehensive test coverage for the Frame module, including basic initialization, execution control, stack operations, gas metering, memory operations, return data management, and context accessors.

Updated the build system to include frame tests in the test suite, with dedicated test steps for both standard and minimal frame tests.

Testing

  • Added extensive unit tests covering all Frame functionality
  • Created integration tests for execution scenarios
  • Added dedicated test steps in the build system
  • Verified gas metering accuracy against EVM specifications
  • Tested error handling for all edge cases (stack overflow/underflow, out of gas, invalid jumps)

Additional Information

Your ENS/address:

Summary by CodeRabbit

  • New Features

    • Introduced a comprehensive execution context for Ethereum Virtual Machine (EVM) call frames, supporting stack, memory, gas tracking, execution control, return data, and context accessors.
    • Added robust error handling and halt reasons for EVM execution.
    • Exposed new types for EVM frame management, including Frame, FrameError, HaltReason, and Gas.
  • Tests

    • Added extensive tests covering frame initialization, execution control, stack and memory operations, gas metering, context access, and debug functionality.

Copy link
changeset-bot bot commented May 28, 2025

⚠️ No Changeset found

Latest commit: c1270a1

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
vercel bot commented May 28, 2025

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

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

Copy link
Contributor
coderabbitai bot commented May 28, 2025

Walkthrough

A new EVM execution context module (Frame.zig) is introduced, encapsulating stack, memory, gas, and execution state management. The build system is updated to include comprehensive test targets for this module, and the main EVM module re-exports the new types. Extensive tests validate all aspects of the Frame functionality.

Changes

File(s) Change Summary
build.zig Added frame-test and frame-minimal-test targets, run steps, and integrated into main tests.
src/evm/Frame.zig New module implementing EVM call frame: stack, memory, gas, control, error types, and methods.
src/evm/evm.zig Imported and re-exported Frame, FrameError, HaltReason, Gas from Frame.zig.
test/Evm/frame_test.zig Added comprehensive tests for Frame covering all behaviors, error handling, and integration.

Sequence Diagram(s)

sequenceDiagram
    participant TestSuite as Test Suite
    participant Frame as Frame
    participant Stack as Stack
    participant Memory as Memory
    participant Gas as Gas
    participant Contract as Contract

    TestSuite->>Frame: init(contract, params)
    Frame->>Stack: initialize
    Frame->>Memory: initialize
    Frame->>Gas: initialize

    TestSuite->>Frame: push(value)
    Frame->>Stack: push(value)

    TestSuite->>Frame: pop()
    Frame->>Stack: pop()

    TestSuite->>Frame: consumeGas(amount)
    Frame->>Gas: consume(amount)

    TestSuite->>Frame: ensureMemory(size)
    Frame->>Memory: expand(size)
    Frame->>Gas: memoryGasCost(size)

    TestSuite->>Frame: setReturnData(data)
    Frame->>Memory: allocate(data)

    TestSuite->>Frame: halt(reason)
    Frame-->>TestSuite: halt_reason set

    TestSuite->>Frame: deinit()
    Frame->>Memory: cleanup
Loading

Possibly related PRs

  • evmts/tevm-monorepo#1695: Refactors and optimizes the Stack implementation used by Frame; directly impacts Frame's core functionality.
  • evmts/tevm-monorepo#1665: Introduces the initial EVM execution frame module and core structures; this PR builds upon that foundation.
  • evmts/tevm-monorepo#1668: Refactors Frame into modular components and updates tests, closely related to the new tests and structure.

Poem

In the meadow of code, a Frame takes its place,
With stacks and with memory, it runs the great race.
Gas ticks away as the opcodes advance,
Tests hop along in a rigorous dance.
Now rabbits rejoice—EVM’s new frame,
Hopping through tests, it’s earning its name!
🐇✨


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
roninjin10 commented May 28, 2025

…context management

Adds a comprehensive execution frame module that manages:
- Stack and memory delegation with efficient operations
- Program counter and execution control (jumps, halts)
- Gas metering with memory expansion cost calculation
- Return data management and buffer handling
- Static mode enforcement for read-only calls
- Call depth tracking for recursion limits
- Debug support with conditional logging

The implementation follows patterns from revm, evmone, and go-ethereum
with optimizations for zero-allocation paths and inline hot functions.

Includes extensive test coverage for all functionality including edge
cases, error conditions, and integration scenarios.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@roninjin10 roninjin10 marked this pull request as ready for review May 28, 2025 16:57
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: 2

🧹 Nitpick comments (2)
test/Evm/frame_test.zig (1)

24-44: Consider extracting test bytecode pattern for better maintainability.

The test bytecode is currently hardcoded inline. While acceptable for tests, consider extracting it as a constant for better readability and reusability across tests.

+const TEST_BYTECODE = [_]u8{ 
+    constants.PUSH1, 0x42, 
+    constants.JUMPDEST, 
+    constants.PUSH1, 0x00, 
+    constants.JUMP, 
+    constants.JUMPDEST, 
+    constants.STOP, 
+    0x00, 0x00 
+};
+
 fn createTestContract(allocator: std.mem.Allocator) !*Contract {
-    const code = try allocator.alloc(u8, 10);
-    @memcpy(code, &[_]u8{ constants.PUSH1, 0x42, constants.JUMPDEST, constants.PUSH1, 0x00, constants.JUMP, constants.JUMPDEST, constants.STOP, 0x00, 0x00 });
+    const code = try allocator.alloc(u8, TEST_BYTECODE.len);
+    @memcpy(code, &TEST_BYTECODE);
src/evm/Frame.zig (1)

170-190: Consider validating depth parameter during initialization.

The frame accepts any depth value but has a limit of 1024. Consider validating this during initialization to fail fast.

Add depth validation:

    pub fn init(
        allocator: std.mem.Allocator,
        contract: *Contract,
        gas: *Gas,
        is_static: bool,
        depth: u32,
    ) !Frame {
+        if (depth >= 1024) {
+            return error.DepthLimit;
+        }
        return Frame{
            .stack = Stack{},
            .memory = try Memory.init(allocator),
            .contract = contract,
            .gas = gas,
            .pc = 0,
            .return_data = null,
            .halt_reason = .None,
            .is_static = is_static,
            .depth = depth,
            .allocator = allocator,
        };
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb67ab and c1270a1.

📒 Files selected for processing (4)
  • build.zig (2 hunks)
  • src/evm/Frame.zig (1 hunks)
  • src/evm/evm.zig (1 hunks)
  • test/Evm/frame_test.zig (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Nx Cloud - Main Job
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: CI Checks
🔇 Additional comments (10)
src/evm/evm.zig (1)

34-39: LGTM! Clean module integration.

The Frame module is properly imported and its key types are re-exported, making them accessible through the main evm module. This follows the established pattern used for other submodules in this file.

build.zig (2)

287-314: Test targets properly configured.

The Frame test targets are correctly set up with appropriate module imports. Both frame-test and frame-minimal-test follow the established patterns for test configuration in this build file.


609-609: Frame tests correctly integrated into test suite.

The frame test is properly added as a dependency to the main test step, ensuring it runs as part of the overall test suite.

test/Evm/frame_test.zig (2)

48-568: Excellent comprehensive test coverage!

The test suite thoroughly covers all aspects of the Frame module:

  • ✅ Proper resource management with defer statements
  • ✅ Edge case testing (stack overflow/underflow, out of gas, invalid jumps)
  • ✅ Gas metering accuracy verification
  • ✅ Memory operation and expansion testing
  • ✅ Integration scenarios simulating real execution
  • ✅ Debug functionality validation

The tests are well-structured, follow consistent patterns, and provide production-quality coverage for the Frame module.


14-14:

❓ Verification inconclusive

Verify the Address import path is correct.

The Address type is imported from a separate "address" module rather than from the evm module. Please confirm this is the intended import path and aligns with the project's module structure.


🏁 Script executed:

#!/bin/bash
# Description: Verify if Address is also exported from evm module or if the separate import is required

# Check if Address is exported from evm module
rg -A 5 "pub const Address" src/evm/evm.zig

# Check for other files importing Address to see the pattern
rg "import.*[Aa]ddress" --type zig

Length of output: 290


EVM module doesn’t export Address; test import path is required

We checked src/evm/evm.zig and found no pub const Address, so pulling in Address from its own module is correct. Other usages in the repo include:

  • src/Signature/eip712.zig@import("../Address/address.zig").Address
  • src/Block/block.zig@import("Address")
  • test/Evm/frame_test.zig@import("address").Address

Please confirm that @import("address") in your test resolves to the same src/Address/address.zig file and aligns with your Zig module-naming conventions. If not, consider normalizing the import path (casing and/or relative path) for consistency.

src/evm/Frame.zig (5)

1-41: Well-structured module setup with comprehensive documentation.

The file header provides excellent context with performance characteristics and reference implementations. The error types comprehensively cover all frame operation failures.


42-62: Comprehensive halt reason enumeration.

The HaltReason enum covers all EVM execution termination scenarios, including normal exits, errors, and edge cases specific to contract creation.


360-386: Excellent return data management.

The implementation properly handles memory allocation and cleanup, with null safety for empty return data.


460-500: Well-designed debug support with zero runtime cost.

The debug functions are properly gated by compile-time checks, ensuring no performance impact in release builds. The logging provides comprehensive frame state information.


318-342: ⚠️ Potential issue

Fix endianness for EVM memory word operations.

EVM uses little-endian byte ordering for memory storage. The current implementation uses big-endian, which will cause incorrect behavior with memory-related opcodes.

Apply these fixes:

    /// Read word from memory
    pub fn readMemoryWord(self: *Frame, offset: usize) !u256 {
        try self.ensureMemory(offset, 32);
        const word = try self.memory.getWord(offset);
        var result: u256 = 0;
        var i: usize = 0;
        while (i < 32) : (i += 1) {
-            result = (result << 8) | word[i];
+            result |= @as(u256, word[i]) << @intCast(i * 8);
        }
        return result;
    }

    /// Write word to memory
    pub fn writeMemoryWord(self: *Frame, offset: usize, value: u256) !void {
        try self.ensureMemory(offset, 32);
        var word: [32]u8 = undefined;
        var val = value;
-        var i: usize = 32;
-        while (i > 0) {
-            i -= 1;
+        var i: usize = 0;
+        while (i < 32) : (i += 1) {
            word[i] = @truncate(val);
            val >>= 8;
        }
        try self.memory.setWord(offset, word);
    }

Likely an incorrect or invalid review comment.

Comment on lines +97 to +100
8000
/// Add gas refund
pub fn refund(self: *Gas, amount: u64) void {
self.refunded = @min(self.refunded + amount, std.math.maxInt(u64) - amount);
}
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

Fix the overflow protection logic in the refund function.

The current logic limits refunds incorrectly. The second parameter to @min should ensure the total doesn't exceed maxInt(u64).

Apply this fix:

-    pub fn refund(self: *Gas, amount: u64) void {
-        self.refunded = @min(self.refunded + amount, std.math.maxInt(u64) - amount);
-    }
+    pub fn refund(self: *Gas, amount: u64) void {
+        const max_refund = std.math.maxInt(u64) - self.refunded;
+        self.refunded += @min(amount, max_refund);
+    }
📝 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
/// Add gas refund
pub fn refund(self: *Gas, amount: u64) void {
self.refunded = @min(self.refunded + amount, std.math.maxInt(u64) - amount);
}
/// Add gas refund
pub fn refund(self: *Gas, amount: u64) void {
const max_refund = std.math.maxInt(u64) - self.refunded;
self.refunded += @min(amount, max_refund);
}
🤖 Prompt for AI Agents
In src/evm/Frame.zig around lines 97 to 100, the refund function's overflow
protection is incorrect because it subtracts the amount from maxInt(u64) instead
of ensuring the sum of refunded and amount does not exceed maxInt(u64). Fix this
by changing the second parameter of @min to maxInt(u64) directly, so the
refunded value is capped at maxInt(u64) without subtracting amount.

Comment on lines +420 to +434
/// Check if valid jump destination
pub fn isValidJump(self: *const Frame, dest: usize) bool {
if (dest >= self.contract.code.len) return false;
if (self.contract.code[dest] != constants.JUMPDEST) return false;
// If contract has advanced analysis, use it
if (self.contract.analysis) |analysis| {
// Check if the jumpdest is in the valid positions
for (analysis.jumpdest_positions) |pos| {
if (pos == dest) return true;
}
return false;
}
// For simple contracts without analysis, JUMPDEST opcode check is sufficient
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Security issue: Invalid jump destination validation without analysis.

The current implementation can incorrectly validate jumps to PUSH data that happens to contain the JUMPDEST byte (0x5B). Without contract analysis, you cannot determine if a JUMPDEST byte is an actual opcode or data.

The safe approach is to require contract analysis for jump validation:

    /// Check if valid jump destination
    pub fn isValidJump(self: *const Frame, dest: usize) bool {
        if (dest >= self.contract.code.len) return false;
        if (self.contract.code[dest] != constants.JUMPDEST) return false;
        // If contract has advanced analysis, use it
        if (self.contract.analysis) |analysis| {
            // Check if the jumpdest is in the valid positions
            for (analysis.jumpdest_positions) |pos| {
                if (pos == dest) return true;
            }
            return false;
        }
-        // For simple contracts without analysis, JUMPDEST opcode check is sufficient
-        return true;
+        // Without analysis, we cannot safely validate jump destinations
+        return false;
    }

To verify this concern, let's check if contract analysis is always performed before execution:


🏁 Script executed:

#!/bin/bash
# Search for contract initialization and analysis patterns
rg -A 10 "Contract.*init|analyze.*contract" --type zig

Length of output: 21215


I want to confirm how Contract.analysis is populated. Let’s inspect the Contract definition and its init method:


🏁 Script executed:

#!/bin/bash
# Locate Contract struct and init function to see how 'analysis' is set
rg "pub const Contract" -A 20 --type zig
rg "pub fn init" -A 20 --type zig

Length of output: 43293


Let’s inspect the validJumpdest implementation to see if and how it populates Contract.analysis before validation:


🏁 Script executed:

#!/bin/bash
# Find and show the validJumpdest method
rg "pub fn validJumpdest" -A20 --type zig

Length of output: 1117


Let’s inspect the ensureAnalysis implementation in src/evm/Contract.zig to see how—and when—it populates self.analysis:


🏁 Script executed:

#!/bin/bash
# Locate and display the ensureAnalysis method
rg "fn ensureAnalysis" -A20 --type zig src/evm/Contract.zig

Length of output: 761


Ensure analysis before validating JUMPDEST in Frame.isValidJump

Frame.isValidJump currently bypasses full analysis whenever contract.analysis is null, accepting any 0x5B byte as a valid jump destination (even if it’s PUSH data). Since contract.analysis only gets populated by calling Contract.ensureAnalysis(), the first JUMP into data still succeeds. We must always invoke analysis before approving a jump.

Locations to update:

  • src/evm/Frame.zig (around lines 420–434)

Proposed change:

     pub fn isValidJump(self: *const Frame, dest: usize) bool {
         if (dest >= self.contract.code.len) return false;
         if (self.contract.code[dest] != constants.JUMPDEST) return false;
+        // Populate valid jump positions before checking
+        self.contract.ensureAnalysis();
         if (self.contract.analysis) |analysis| {
             for (analysis.jumpdest_positions) |pos| {
                 if (pos == dest) return true;
             }
             return false;
         }
-        // For simple contracts without analysis, JUMPDEST opcode check is sufficient
-        return true;
+        // No analysis available ⇒ disallow jump
+        return false;
     }
📝 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
/// Check if valid jump destination
pub fn isValidJump(self: *const Frame, dest: usize) bool {
if (dest >= self.contract.code.len) return false;
if (self.contract.code[dest] != constants.JUMPDEST) return false;
// If contract has advanced analysis, use it
if (self.contract.analysis) |analysis| {
// Check if the jumpdest is in the valid positions
for (analysis.jumpdest_positions) |pos| {
if (pos == dest) return true;
}
return false;
}
// For simple contracts without analysis, JUMPDEST opcode check is sufficient
return true;
}
/// Check if valid jump destination
pub fn isValidJump(self: *const Frame, dest: usize) bool {
if (dest >= self.contract.code.len) return false;
if (self.contract.code[dest] != constants.JUMPDEST) return false;
// Populate valid jump positions before checking
self.contract.ensureAnalysis();
if (self.contract.analysis) |analysis| {
// Check if the jumpdest is in the valid positions
for (analysis.jumpdest_positions) |pos| {
if (pos == dest) return true;
}
return false;
}
// No analysis available ⇒ disallow jump
return false;
}
🤖 Prompt for AI Agents
In src/evm/Frame.zig around lines 420 to 434, the isValidJump function currently
returns true if the byte at the destination is JUMPDEST without requiring
contract analysis, which can incorrectly validate jumps into PUSH data. To fix
this, modify isValidJump to always require that contract.analysis is present by
calling Contract.ensureAnalysis() if analysis is null before checking
jumpdest_positions. This ensures that jump validation only succeeds when full
contract analysis is available, preventing invalid jump destinations.

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