Skip to content

feat(evm): add EIP-7939 CLZ opcode (0x1e)#1709

Open
snissn wants to merge 6 commits intomasterfrom
eip7939-clz
Open

feat(evm): add EIP-7939 CLZ opcode (0x1e)#1709
snissn wants to merge 6 commits intomasterfrom
eip7939-clz

Conversation

@snissn
Copy link
Copy Markdown
Contributor

@snissn snissn commented Jan 21, 2026

Summary

Implements Ethereum EIP-7939 (Count Leading Zeros) for the FEVM EVM interpreter by adding the CLZ opcode at byte 0x1e.

Changes

  • Adds CLZ (0x1e) to the opcode table and instruction dispatch.
  • Implements CLZ semantics over 256-bit words (CLZ(0) = 256).
  • Adds tests covering the EIP-7939 test vectors:
    • unit + bytecode-level tests in the interpreter
    • end-to-end actor test executing CLZ in a deployed EVM actor

Notes

  • Full workspace cargo test may require a wasm32-capable clang; on macOS this can be run with CC=/opt/homebrew/opt/llvm/bin/clang cargo test.

Related

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements EIP-7939 (Count Leading Zeros) for the FEVM EVM interpreter by adding the CLZ opcode at byte position 0x1e. The opcode counts the number of leading zero bits in a 256-bit word, returning 256 when the input is zero.

Changes:

  • Added CLZ opcode (0x1e) to the opcode dispatch table
  • Implemented CLZ semantics using U256's leading_zeros() method
  • Added comprehensive test coverage including unit tests, bytecode-level tests, and end-to-end actor tests

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
actors/evm/src/interpreter/execution.rs Adds CLZ opcode at 0x1e to the opcode table and includes it in underflow testing
actors/evm/src/interpreter/instructions/mod.rs Defines CLZ instruction using the def_primop! macro with single operand
actors/evm/src/interpreter/instructions/bitwise.rs Implements clz function and includes unit tests and bytecode-level integration tests
actors/evm/tests/eip7939_clz.rs Adds end-to-end test with EIP-7939 test vectors using a deployed EVM actor

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@BigLep BigLep moved this from 📌 Triage to 🔎 Awaiting Review in FilOz Jan 27, 2026
@BigLep BigLep requested a review from LesnyRumcajs January 27, 2026 04:32
@BigLep
Copy link
Copy Markdown
Member

BigLep commented Jan 27, 2026

@LesnyRumcajs : are you or someone on Forest able to take a look at this one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: this PR doesn't need to change the lockfile (fine to do it in a separate PR); especially the syn major version downgrade is suspicious.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1, please remove lockfile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed Cargo.lock from this PR. The ruint/syn drift was unrelated to CLZ and shouldn't ride along here.

Comment on lines +46 to +49
#[inline]
pub fn clz(value: U256) -> U256 {
U256::from(value.leading_zeros())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: it might be obvious to some, but my brain doesn't immediately click that clz is count leading zeroes. One line of docs would reduce mental overhead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1 on this. Try /// docstring. This shows up in editors cleanly.

I see that this is also okay in a way that EIP calling it "CLZ", so should be fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a /// doc comment on clz clarifying that it implements EIP-7939 Count Leading Zeroes over a 256-bit word, with CLZ(0) = 256.

mod tests {
use crate::evm_unit_test;

#[test]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think it's a valuable test. If we want to check the def_opcodes macro works, lets write a test to for it entirely.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I second this. Introduce this only if other opcodes do it as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the standalone test_clz_opcode_value check. The execution underflow coverage still exercises CLZ alongside the other opcodes.

Copy link
Copy Markdown

@redpanda-f redpanda-f left a comment

Choose a reason for hiding this comment

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

Thank you so much for this work!

I second what @LesnyRumcajs has already pointed out but would really like these my suggestions to be incorporated for maintainability.

Comment on lines +46 to +49
#[inline]
pub fn clz(value: U256) -> U256 {
U256::from(value.leading_zeros())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1 on this. Try /// docstring. This shows up in editors cleanly.

I see that this is also okay in a way that EIP calling it "CLZ", so should be fine.

}
}

#[inline]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
#[inline]
#[inline]
/// Implements EIP-7939 `CLZ` opcode, returns number of leading zero bits

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied this in spirit. I kept the doc comment a bit more explicit and also called out CLZ(0) = 256.


#[test]
fn test_clz_eip7939_vectors_unit() {
// Directly matches the EIP-7939 test cases.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Given this, kindly add a link for future reference on where the test cases were borrowed from.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a shared test_vectors.rs helper and linked it to the EIP so the provenance is explicit.

}

#[test]
fn test_clz_eip7939_vectors() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Kindly remove. This seems like a copy of what you have already done earlier in the file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

the main difference is clz_via_evm, so instead I suggest renaming the test

Suggested change
fn test_clz_eip7939_vectors() {
fn test_clz_eip7939_vectors_via_evm() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept the machine-level coverage, renamed it to test_clz_eip7939_vectors_via_evm, and switched it to the shared vectors so it's no longer a copy/paste duplicate.

assert_eq!(clz(U256::ONE), U256::from(255));
}

#[test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not entirely sure of this test. Either we introduce a wide range of tests, or a fuzzy test here, or none at all. This seems quite arbitrary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the extra test_clz_misc_unit case. The remaining unit, machine-step, and end-to-end coverage now all use the canonical EIP-7939 vectors.

mod tests {
use crate::evm_unit_test;

#[test]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I second this. Introduce this only if other opcodes do it as well.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1, please remove lockfile.

let rt = util::construct_and_verify(initcode);

let vectors = [
(U256::ZERO, U256::from(256)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These combinations are used often, once in unit test and here as well. Kindly don't repeat yourself, hoist it in some "test_vectors.rs" or something. Otherwise, it will go out of sync later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hoisted the vectors into shared tests/test_vectors.rs, so the unit and end-to-end tests stay in sync.

@github-project-automation github-project-automation bot moved this from 🔎 Awaiting Review to ⌨️ In Progress in FilOz Feb 24, 2026
CODECOPY,
CREATE,
CREATE2,
CLZ,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

the rest of these opcodes seem to be mostly in sequential order, so this belongs after SAR instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adjusted the execution underflow list so CLZ now sits after SAR with the other bitwise opcodes.

Comment on lines +10 to +17
// PUSH2 len
// PUSH2 offset
// PUSH1 0x00
// CODECOPY
// PUSH2 len
// PUSH1 0x00
// RETURN
// <runtime bytes>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There's a shorter initcode prefix that doesn't parameterize len, sometimes called the universal runtime constructor.

SUB(CODESIZE,11)
CODECOPY(RETURNDATASIZE,11,DUP1)
RETURNDATASIZE
RETURN

600b380380600b3d393df3

(It uses RETURNDATASIZE for backwards compatibility since not all chains have PUSH0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched the end-to-end initcode to the universal runtime constructor from your link.

Comment on lines +48 to +49
opcodes::PUSH1,
0x00,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think you should prefer PUSH0 over PUSH1 0x00.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced the zero literals in the CLZ runtime with PUSH0.

Comment on lines +55 to +56
opcodes::PUSH1,
0x20,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MSIZE

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used MSIZE for the return length after MSTORE.

@wjmelements
Copy link
Copy Markdown

How does activation work? How do we enable the opcode only after a certain network version?

@rvagg
Copy link
Copy Markdown
Member

rvagg commented Feb 25, 2026

How does activation work? How do we enable the opcode only after a certain network version?

The code in here is all latest, unlike go-state-types which has a directory for each network version, or lotus which maintains the historical versions of actors all the way back through to genesis, this code on master and what will eventually be tagged will be bundled up, put into Lotus, Forest and Venus as the code for the next network version. So v18.0.0 will have an associated WASM bundle, in CAR format, compressed and made available in the validator nodes. In Lotus, we have a "schedule" that lists what epoch a particular network version is activate and what actors version is associated with that network version. So nv28 will be associated with actors v18. Then when the validator is executing blocks for a given epoch it does that mapping and will load the actor code it needs and just blindly run it.

So there's (almost) no epoch or network version aware code in builtin-actors, it just cares about "state in, state out" for any particular message it needs to process. Implementing CLZ here just means that any message in using this code gets to use it, but any builtin-actors run before now won't. We hide (almost) all of network version switching logic in Lotus/Forest/Venus, and there's a lot more than just builtin-actors versions obviously, it's a huge amount of technical debt that keeps building up. But as long as we persist with "Lotus needs to be able to run the network from genesis" then it's not going away.

@snissn
Copy link
Copy Markdown
Contributor Author

snissn commented Mar 10, 2026

Addressed the review feedback in 270bfcd0.

  • removed the unrelated lockfile diff
  • added clz docs
  • hoisted the EIP-7939 vectors into a shared helper with a source link
  • trimmed the extra tests and renamed the machine-level one
  • simplified the end-to-end bytecode (PUSH0, MSIZE, universal runtime constructor)

Replied inline on the individual review threads as well.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.51%. Comparing base (2a06959) to head (270bfcd).
⚠️ Report is 17 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1709      +/-   ##
==========================================
- Coverage   91.23%   90.51%   -0.72%     
==========================================
  Files         151      139      -12     
  Lines       32006    27638    -4368     
==========================================
- Hits        29200    25017    -4183     
+ Misses       2806     2621     -185     
Files with missing lines Coverage Δ
actors/evm/src/interpreter/execution.rs 93.42% <ø> (+1.27%) ⬆️
actors/evm/src/interpreter/instructions/bitwise.rs 100.00% <100.00%> (ø)
actors/evm/src/interpreter/instructions/mod.rs 100.00% <ø> (ø)

... and 132 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ⌨️ In Progress

Development

Successfully merging this pull request may close these issues.

8 participants