8000 ✨ feat(evm): implement production-quality Journal module for state change tracking by roninjin10 · Pull Request #1738 · 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 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

Merged
merged 2 commits into from
May 28, 2025

Conversation

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

Description

Implemented a Journal system for the EVM to track and revert state changes during transaction execution. The Journal provides:

  • Checkpoint/revert/commit functionality for nested calls
  • Recording of various state changes (accounts, balances, storage, etc.)
  • EIP-2929 access list tracking for warm/cold accounts and storage slots
  • Log management with proper memory handling
  • Gas refund tracking

Testing

Added comprehensive test suite covering:

  • Basic initialization and cleanup
  • Checkpoint management
  • State change recording
  • Access list functionality
  • Log management
  • Refund tracking
  • Memory management and leak detection
  • Nested checkpoints and reverts

Additional Information

Your ENS/address:

Summary by CodeRabbit

  • New Features

    • Introduced a journaling system for Ethereum Virtual Machine (EVM) state changes, supporting checkpointing, reverting, committing, and gas refund tracking.
    • Added access list management for accounts and storage slots, log recording, and robust state rollback capabilities.
  • Tests

    • Added a comprehensive test suite covering journal initialization, checkpoint management, state change tracking, access list handling, log management, refund accounting, storage clearing, nested checkpoints, and memory leak detection.
  • Chores

    • Integrated the new journal tests into the automated build and test process.

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 6:28pm
tevm-monorepo-app ✅ Ready (Inspect) Visit Preview May 28, 2025 6:28pm
1 Skipped Deployment
Name Status Preview Updated (UTC)
tevm-monorepo-tevm ⬜️ Ignored (Inspect) May 28, 2025 6:28pm

Copy link
changeset-bot bot commented May 28, 2025

⚠️ No Changeset found

Latest commit: a80e2cd

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.

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 28, 2025

Walkthrough

A 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

File(s) Change Summary
src/evm/Journal.zig New module implementing a full-featured EVM journaling system with checkpoints, reverts, logs, etc.
src/evm/evm.zig Imports and publicly re-exports Journal types and constants from Journal.zig.
test/Evm/journal_test.zig Adds comprehensive tests for Journal covering all major features and edge cases.
build.zig Adds 8000 a new test target and step for journal tests; integrates them into the main test workflow.

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()
Loading

Poem

In the warren of code, a journal appears,
To track all the changes, the state and the years.
With checkpoints and logs, and refunds in tow,
It hops through the EVM, wherever we go.
Tests keep it honest, no leaks in the night—
This rabbit’s new journal is working just right!
🐇✨


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between db76a0e and a80e2cd.

📒 Files selected for processing (4)
  • build.zig (2 hunks)
  • src/evm/Journal.zig (1 hunks)
  • src/evm/evm.zig (1 hunks)
  • test/Evm/journal_test.zig (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • build.zig
  • src/evm/evm.zig
  • src/evm/Journal.zig
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Nx Cloud - Main Job
  • GitHub Check: CI Checks
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (13)
test/Evm/journal_test.zig (13)

1-11: Excellent imports and module setup!

The imports are well-organized and include all necessary types from the EVM module. The use of specific type imports (Journal, Change, Checkpoint, etc.) makes the test code more readable and ensures type safety.


12-36: Comprehensive initialization testing with proper memory management.

The test thoroughly covers both basic initialization and initialization with capacity. The capacity checks on lines 32-34 correctly use >= since the implementation may allocate more than the minimum requested capacity. The use of defer statements ensures proper cleanup.


38-69: Excellent checkpoint management testing with edge case coverage.

This test comprehensively covers the checkpoint lifecycle including nested checkpoints, commits, reverts, and proper error handling for invalid operations. The error testing on lines 65-68 ensures that attempting to revert or commit without active checkpoints properly returns the expected JournalError.NoCheckpoint.


71-108: Thorough state change recording validation.

The test covers all major types of state changes that the Journal needs to track. The consistent use of test data (e.g., [_]u8{1} ** 20 for addresses) and proper verification of change counts after each operation demonstrates good testing practices.


110-139: Well-designed checkpoint and revert interaction testing.

This test effectively validates the crucial interaction between checkpoints and state changes, ensuring that reverts properly restore the journal to the checkpointed state while preserving earlier changes. The verification on lines 137-139 confirms the correct number of remaining changes.


141-193: Excellent EIP-2929 access list implementation testing.

This comprehensive test validates the warm/cold account and storage slot tracking required by EIP-2929. The test correctly verifies that:

  • Initial accesses return cold (true)
  • Subsequent accesses return warm (false)
  • Access list state is properly maintained across checkpoints and reverts
  • Different addresses and storage slots are tracked independently

This is critical functionality for gas cost calculations.


195-229: Comprehensive log management testing with memory safety.

The test validates log recording, proper data storage, checkpoint/revert behavior, and log clearing functionality. The verification of log contents on lines 214-216 ensures data integrity, and the checkpoint/revert testing confirms logs are properly managed during state transitions.


231-261: Robust refund management with underflow protection.

The test thoroughly covers gas refund tracking including addition, subtraction, and the important underflow protection on lines 248-249 that prevents refunds from going negative. The checkpoint/revert testing ensures refund state is properly managed during transaction rollbacks.


263-297: Well-implemented storage clearing test with proper verification.

The test validates the StorageCleared change type by creating actual storage data and verifying that the recorded change contains the correct address and storage mapping. The switch statement on lines 288-296 properly validates the change structure and data integrity.


299-332: Comprehensive clear operation validation.

This test ensures that the clear() operation properly resets all journal state including changes, checkpoints, refunds, logs, and access lists. The thorough verification before and after clearing on lines 315-331 confirms complete state reset.


334-374: Excellent nested checkpoint scenario testing.

This test validates complex checkpoint nesting with commits and reverts, which is crucial for handling nested EVM calls. The verification that only the first change remains after the operations (lines 367-373) confirms correct checkpoint boundary management.


376-435: Outstanding memory leak detection and safety testing.

This test is particularly valuable as it validates proper memory management for operations that allocate memory (code changes, logs, storage clearing, access lists). The use of the testing allocator ensures any memory leaks would be detected. The two test blocks cover both normal cleanup and revert-based cleanup scenarios.


1-435: Exceptional test suite quality and coverage.

This is an exemplary test suite that comprehensively covers all Journal functionality with:

Complete feature coverage: All Journal operations including state changes, checkpoints, access lists, logs, and refunds
Memory safety: Proper use of defer statements and testing allocator for leak detection
Edge case testing: Error conditions, underflow protection, and complex nested scenarios
EIP-2929 compliance: Thorough testing of warm/cold access tracking
Clear structure: Well-organized tests with descriptive names and consistent patterns
Data integrity: Verification of stored data correctness across all operations

The test suite provides strong confidence in the Journal module's correctness and robustness for production use in the EVM implementation.


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

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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 100046a and db76a0e.

📒 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 calling freeChange 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 and false for warm access aligns with gas cost calculation requirements.


137-148: ⚠️ Potential issue

Fix 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 with defer 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 ordering

The 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

Comment on lines +194 to +195
const change = self.changes.pop() orelse unreachable;
try self.applyRevert(&change);
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

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.

Comment on lines 13 to 14
// Import Address separately since it's not part of evm module
const Address = @import("address").Address;
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

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:

  1. Moving Address to a common module that both Frame and tests can import, or
  2. 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.

Comment on lines 421 to 434
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.

⚠️ Potential issue

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.

Suggested 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;
// 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.

roninjin10 and others added 2 commits May 28, 2025 10:57
…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.
@roninjin10 roninjin10 merged commit b384ba1 into main May 28, 2025
10 checks passed
@roninjin10 roninjin10 deleted the evm-journal branch May 28, 2025 18:43
@roninjin10 roninjin10 mentioned this pull request May 28, 2025
1 task
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