-
-
Notifications
You must be signed in to change notification settings - Fork 41
✨ feat(evm): implement production-quality Journal module for state change tracking #1738
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
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
|
WalkthroughA journaling system for Ethereum Virtual Machine (EVM) state changes was implemented in a new module, providing checkpoint, revert, commit, and log management functionality. The module is integrated into the build system with dedicated tests, and its API is re-exported for use elsewhere. Comprehensive tests validate initialization, state changes, access lists, logs, refunds, and memory management. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Journal
Caller->>Journal: init(allocator)
Caller->>Journal: checkpoint()
Caller->>Journal: recordStateChange(...)
Caller->>Journal: recordLog(...)
Caller->>Journal: addRefund(amount)
alt Revert scenario
Caller->>Journal: revert()
Journal-->>Caller: Undo changes, logs, refunds
else Commit scenario
Caller->>Journal: commit()
Journal-->>Caller: Discard checkpoint
end
Caller->>Journal: getLogs()
Caller->>Journal: clear()
Caller->>Journal: deinit()
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
⏰ Context from checks skipped due to timeout of 90000ms (3)
🔇 Additional comments (13)
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/evm/Journal.zig (2)
388-388
: Add documentation for the negative refund encoding.The encoding scheme for negative refunds is clever but not immediately obvious. Consider adding a comment to explain the logic.
// Record negative refund change + // We encode negative refunds as (maxInt - amount + 1) to distinguish them from positive refunds try self.changes.append(.{ .Refund = .{ .amount = std.math.maxInt(u64) - actual_amount + 1 } });
446-446
: Consider using a named constant for refund encoding threshold.The magic number check for distinguishing positive/negative refunds could be clearer with a named constant.
+const REFUND_SUBTRACT_THRESHOLD = std.math.maxInt(u64) / 2; + fn applyRevert(self: *Journal, change: *const Change) !void { switch (change.*) { // ... other cases ... .Refund => |c| { // Revert refund changes - if (c.amount > std.math.maxInt(u64) / 2) { + if (c.amount > REFUND_SUBTRACT_THRESHOLD) { // This was a subtraction, add it back self.refund +|= std.math.maxInt(u64) - c.amount + 1; } else {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
build.zig
(3 hunks)src/evm/Frame.zig
(1 hunks)src/evm/Journal.zig
(1 hunks)src/evm/evm.zig
(1 hunks)test/Evm/frame_test.zig
(1 hunks)test/Evm/journal_test.zig
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Graphite / mergeability_check
🔇 Additional comments (11)
src/evm/Journal.zig (3)
150-175
: Excellent memory management implementation!The
deinit
function comprehensively frees all allocated memory, including nested structures. The pattern of iterating through changes and callingfreeChange
ensures no memory leaks.
308-336
: Correct EIP-2929 access list implementation!The warm/cold tracking for accounts and storage slots properly implements EIP-2929 semantics. The pattern of returning
true
for cold access andfalse
for warm access aligns with gas cost calculation requirements.
137-148
:⚠️ Potential issueFix ArrayList initialization with capacity.
The
initCapacity
function returns an error union, but you're trying to assign it directly to struct fields. This will cause a compilation error.Apply this diff to fix the initialization:
pub fn initWithCapacity(allocator: std.mem.Allocator, capacity: usize) !Journal { - const journal = Journal{ - .changes = try std.ArrayList(Change).initCapacity(allocator, capacity), - .checkpoints = try std.ArrayList(Checkpoint).initCapacity(allocator, capacity / 10), - .logs = try std.ArrayList(LogEntry).initCapacity(allocator, capacity / 5), - .refund = 0, - .warm_accounts = std.AutoHashMap(Address, void).init(allocator), - .warm_storage = std.AutoHashMap(Address, std.AutoHashMap(B256, void)).init(allocator), - .allocator = allocator, - }; - return journal; + var journal = Journal{ + .changes = std.ArrayList(Change).init(allocator), + .checkpoints = std.ArrayList(Checkpoint).init(allocator), + .logs = std.ArrayList(LogEntry).init(allocator), + .refund = 0, + .warm_accounts = std.AutoHashMap(Address, void).init(allocator), + .warm_storage = std.AutoHashMap(Address, std.AutoHashMap(B256, void)).init(allocator), + .allocator = allocator, + }; + try journal.changes.ensureTotalCapacity(capacity); + try journal.checkpoints.ensureTotalCapacity(capacity / 10); + try journal.logs.ensureTotalCapacity(capacity / 5); + return journal; }Likely an incorrect or invalid review comment.
src/evm/evm.zig (1)
34-48
: Clean module organization and exports!The new Frame and Journal modules are properly imported and their key types are re-exported, making them available as part of the EVM package's public API.
build.zig (1)
287-314
: Test infrastructure properly integrated!The new test targets for Frame and Journal modules follow the established pattern and are correctly integrated into the build system. The tests will run as part of the main test suite.
Also applies to: 571-583, 623-624
test/Evm/journal_test.zig (1)
1-435
: Excellent comprehensive test coverage!The test suite thoroughly covers all Journal functionality including:
- Initialization and cleanup with memory leak detection
- Checkpoint management with nested scenarios
- State change recording and reverting
- EIP-2929 access list functionality
- Log management
- Refund accounting
- Edge cases and error conditions
The use of
testing.allocator
ensures memory leaks are detected, and all resources are properly cleaned up withdefer
statements.test/Evm/frame_test.zig (1)
16-568
: Well-structured and comprehensive Frame tests!The test suite provides excellent coverage including:
- Frame lifecycle (initialization and cleanup)
- Stack operations with error conditions
- Memory operations with gas accounting
- Jump validation
- Return data management
- Static mode enforcement
- Integration scenarios
The test helpers and debug functionality tests add good value.
src/evm/Frame.zig (4)
1-28
: Well-documented module with clear performance considerations!The module documentation clearly outlines the purpose, performance characteristics, and references to other implementations. Good architectural approach.
65-145
: Robust gas tracking implementation!The Gas struct correctly implements:
- Overflow-safe gas consumption
- EVM memory expansion cost formula (linear + quadratic components)
- Proper word size alignment (32-byte boundaries)
319-342
: Memory word operations correctly implement big-endian byte orderingThe read and write operations properly handle the EVM's big-endian byte ordering for 256-bit words in memory.
435-501
: Solid implementation of state inspection and debug support!The remaining Frame methods are well-implemented:
- State inspection methods provide clean APIs
- Debug functionality is properly conditioned on build mode
- Resource management is handled correctly throughout
const change = self.changes.pop() orelse unreachable; | ||
try self.applyRevert(&change); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider safer error handling instead of unreachable
.
While the logic ensures items exist, using unreachable
can cause undefined behavior if there's ever a logic error. Consider using array indexing for better safety.
- const change = self.changes.pop() orelse unreachable;
- try self.applyRevert(&change);
+ const change = self.changes.items[self.changes.items.len - 1];
+ try self.applyRevert(&change);
+ _ = self.changes.pop();
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/evm/Journal.zig at lines 194 to 195, replace the use of `pop() orelse
unreachable` with safer error handling by using array indexing to access the
last element of `self.changes`. This avoids potential undefined behavior if the
array is unexpectedly empty. Adjust the code to check the array length before
accessing and handle the empty case appropriately instead of assuming it is
unreachable.
test/Evm/frame_test.zig
Outdated
// Import Address separately since it's not part of evm module | ||
const Address = @import("address").Address; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Address type import inconsistency
The comment states "Import Address separately since it's not part of evm module", but in src/evm/Frame.zig
line 28, Address is defined as part of the Frame module. This creates an inconsistency in how Address is handled across the codebase.
Consider either:
- Moving Address to a common module that both Frame and tests can import, or
- Exporting Address from the evm module so tests can use
evm.Address
🤖 Prompt for AI Agents
In test/Evm/frame_test.zig around lines 13 to 14, the Address type is imported
separately with a comment stating it is not part of the evm module, but in
src/evm/Frame.zig line 28, Address is defined within the Frame module. To fix
this inconsistency, either move the Address type to a common module accessible
by both Frame and the tests, or modify the evm module to export Address so tests
can import it as evm.Address, ensuring consistent usage across the codebase.
src/evm/Frame.zig
Outdated
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix jump validation logic for contracts with empty analysis
The current implementation has a flaw: if contract.analysis
exists but jumpdest_positions
is empty, all jumps will be rejected even if they target valid JUMPDEST opcodes. This could happen if analysis was performed but found no jump destinations.
Consider this fix to handle empty analysis:
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| {
+ // If analysis has no jump destinations but contract has JUMPDEST, allow it
+ if (analysis.jumpdest_positions.len == 0) return true;
// 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;
}
📝 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.
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; | |
} | |
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| { | |
// If analysis has no jump destinations but contract has JUMPDEST, allow it | |
if (analysis.jumpdest_positions.len == 0) return true; | |
// 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; | |
} |
🤖 Prompt for AI Agents
In src/evm/Frame.zig around lines 421 to 434, the jump validation logic
incorrectly rejects all jumps when contract.analysis exists but
jumpdest_positions is empty, even if the destination opcode is a valid JUMPDEST.
To fix this, modify the logic to return true if jumpdest_positions is empty,
allowing jumps to valid JUMPDEST opcodes in this case. This ensures that
contracts with empty analysis still validate jumps correctly.
…cution Adds comprehensive Contract module with advanced optimizations: - JUMPDEST validation with binary search and caching - Storage access tracking for EIP-2929 gas accounting - Memory pooling to reduce allocation pressure - Inline hot-path functions for performance - Gas refund management with EIP-3529 compliance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
…ange tracking Implements a comprehensive transaction journal for tracking and reverting state changes during EVM execution. The Journal supports: - Nested checkpoint management for call hierarchy - Complete state change tracking (account creation/destruction, balance/nonce/code/storage changes) - Access list management for EIP-2929 warm/cold tracking - Log entry management with efficient memory handling - Gas refund tracking - Memory-efficient change storage with proper cleanup - Comprehensive test coverage including memory leak detection The implementation follows patterns from revm and go-ethereum for production quality and performance.
Description
Implemented a Journal system for the EVM to track and revert state changes during transaction execution. The Journal provides:
Testing
Added comprehensive test suite covering:
Additional Information
Your ENS/address:
Summary by CodeRabbit
New Features
Tests
Chores