Skip to content

[DO NOT MERGE] feat: EIP-7939 Implement counting leading zero operation #8771

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

Gabriel-Trintinalia
Copy link
Contributor

@Gabriel-Trintinalia Gabriel-Trintinalia commented Jun 9, 2025

Implements https://eips.ethereum.org/EIPS/eip-7939

This pull request adds support for the EIP-7939 CLZ opcode, implementing the operation itself, adding corresponding tests, and properly registering the new opcode in the mainnet EVM operations.

Introduces CountLeadingZerosOperation which computes the number of leading zeros for a given input.
Provides parameterized tests to verify correct opcode functionality.
Updates MainnetEVMs to register the new CLZ operation.

Signed-off-by: Gabriel-Trintinalia <[email protected]>
Copy link

@Copilot 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 pull request adds support for the EIP-7939 CLZ opcode, implementing the operation itself, adding corresponding tests, and properly registering the new opcode in the mainnet EVM operations.

  • Introduces CountLeadingZerosOperation which computes the number of leading zeros for a given input.
  • Provides parameterized tests to verify correct opcode functionality.
  • Updates MainnetEVMs to register the new CLZ operation.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
evm/src/test/java/org/hyperledger/besu/evm/operation/CountLeadingZerosOperationTest.java Adds tests for CLZ opcode functionality.
evm/src/main/java/org/hyperledger/besu/evm/operation/CountLeadingZerosOperation.java Implements the CLZ operation following EIP-7939.
evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java Registers the new opcode and cleans up obsolete imports.
Comments suppressed due to low confidence (1)

evm/src/main/java/org/hyperledger/besu/evm/operation/CountLeadingZerosOperation.java:28

  • [nitpick] Consider renaming 'clzSuccess' to 'CLZ_SUCCESS' to better reflect that it is a constant value.
static final OperationResult clzSuccess = new OperationResult(3, null);

@macfarla macfarla added the Osaka Osaka fork related - part of Fusaka label Jun 9, 2025
@Gabriel-Trintinalia Gabriel-Trintinalia changed the title feat: EIP-7939 Implement counting leading zero operation [DO NOT MERGE] feat: EIP-7939 Implement counting leading zero operation Jun 9, 2025
Signed-off-by: Gabriel-Trintinalia <[email protected]>
Copy link
Contributor

@daniellehrner daniellehrner left a comment

Choose a reason for hiding this comment

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

We need to add a case 0x1e in the switch statement EVM.runToHalt() in order to be able to be called by the EVM

Signed-off-by: Gabriel-Trintinalia <[email protected]>
Signed-off-by: Gabriel-Trintinalia <[email protected]>
@@ -252,6 +255,10 @@ public void runToHalt(final MessageFrame frame, final OperationTracer tracing) {
case 0x18 -> XorOperation.staticOperation(frame);
case 0x19 -> NotOperation.staticOperation(frame);
case 0x1a -> ByteOperation.staticOperation(frame);
case 0x1e ->
enableOsaka
Copy link
Member

@lu-pinto lu-pinto Jun 18, 2025

Choose a reason for hiding this comment

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

why can't this be part of the ProtocolSpecs? Don't like to have fork specific info inside these classes. It seems we can load all of the opcodes during configuration. But maybe this is a bigger refactoring than this PR should address

Copy link
Member

@lu-pinto lu-pinto left a comment

Choose a reason for hiding this comment

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

Please add an end to end test for this opcode to make sure it fully works for the fork. Suggestion: look into EvmToolSpecTests

@Gabriel-Trintinalia
Copy link
Contributor Author

Please add an end to end test for this opcode to make sure it fully works for the fork. Suggestion: look into EvmToolSpecTests

@lu-pinto done

@ahamlat
Copy link
Contributor

ahamlat commented Jun 19, 2025

Please don't merge before performance benchmarking.

public static OperationResult staticOperation(final MessageFrame frame) {
final Bytes value = frame.popStackItem();
final int numberOfLeadingZeros = UInt256.fromBytes(value).numberOfLeadingZeros();
frame.pushStackItem(Words.intBytes(numberOfLeadingZeros));
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's benchmark two different implementations, the one suggested here and this one
frame.pushStackItem(UInt256.valueOf(value.numberOfLeadingZeros()))

Copy link
Contributor Author

@Gabriel-Trintinalia Gabriel-Trintinalia Jun 19, 2025

Choose a reason for hiding this comment

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

@ahamlat

Let's benchmark two different implementations, the one suggested here and this one
frame.pushStackItem(UInt256.valueOf(value.numberOfLeadingZeros()))

Thanks for looking into this. The reference tests do not pass with Bytes.numberOfLeadingZeros(). I initially was using it but replaced in this commit: e82709e

Copy link
Member

Choose a reason for hiding this comment

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

do you know why were they not passing? The implementation is literally the same - I'm surprised

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lu-pinto It works if Bytes.size() == 32. Wrapping into a Bytes32 works fine.

Copy link
Member

Choose a reason for hiding this comment

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

Ok so @ahamlat you could probably do:

final int numberOfLeadingZeros = Bytes32.wrap(value).numberOfLeadingZeros();
frame.pushStackItem(Words.intBytes(numberOfLeadingZeros));

but I agree, my guess, is less performant.

@Gabriel-Trintinalia Gabriel-Trintinalia self-assigned this Jun 20, 2025
@Gabriel-Trintinalia Gabriel-Trintinalia moved this to In Progress in Osaka Jun 20, 2025
@Gabriel-Trintinalia
Copy link
Contributor Author

Gabriel-Trintinalia commented Jun 25, 2025

@ahamlat @lu-pinto any updates on the benchmarking? Is there any reason why we couldn't merge it as it is and revisit it when the benchmark is done?

@lu-pinto
Copy link
Member

So I ran some benchmarks for both cases (I actually had to change the suggestion implementation slightly to make it work):

      int numberOfLeadingZeros = value.numberOfLeadingZeros() + Bytes32.SIZE - value.size();

Benchmark results show 2-3x slower for the impl in this PR:

Benchmark                                                                                                     (bytesHex)  (useBase32Wrapping)  Mode  Cnt   Score   Error  Units
CountLeadingZerosOperationBenchmark.executeOperation                                                                0x23                 true  avgt   10  14.550 ± 0.017  ms/op
CountLeadingZerosOperationBenchmark.executeOperation                                                                0x23                false  avgt   10  48.058 ± 0.114  ms/op
CountLeadingZerosOperationBenchmark.executeOperation  0x2323232323232323232323232323232323232323232323232323232323232323                 true  avgt   10  15.090 ± 0.068  ms/op
CountLeadingZerosOperationBenchmark.executeOperation  0x2323232323232323232323232323232323232323232323232323232323232323                false  avgt   10  26.924 ± 0.104  ms/op
CountLeadingZerosOperationBenchmark.executeOperation                                    0x232323232323232323232323232323                 true  avgt   10  14.553 ± 0.019  ms/op
CountLeadingZerosOperationBenchmark.executeOperation                                    0x232323232323232323232323232323                false  avgt   10  47.515 ± 0.207  ms/op

branch where I got my benchmarks published: lu-pinto/benchmark-8770-clz-opcode

@ahamlat
Copy link
Contributor

ahamlat commented Jun 26, 2025

We can even simplify the code and avoid the check.
Instead of

numberOfLeadingZeros = value.numberOfLeadingZeros();
      if (value.size() != Bytes32.SIZE) {
        numberOfLeadingZeros += Bytes32.SIZE - value.size();
      }

we can have, as in the case where the check is false, value.size() is called twice, and we already that this call can be time consuming

numberOfLeadingZeros = value.numberOfLeadingZeros() +  Bytes32.SIZE - value.size();

@Gabriel-Trintinalia
Copy link
Contributor Author

@lu-pinto @ahamlat what are the next steps here?

@ahamlat
Copy link
Contributor

ahamlat commented Jul 3, 2025

The benchmarks show that when useBase32Wrapping is true, the results is much better in terms of performance. Which means that the fast implementation is this one

 numberOfLeadingZeros = value.numberOfLeadingZeros();
      if (value.size() != Bytes32.SIZE) {
        numberOfLeadingZeros += Bytes32.SIZE - value.size();
      }

Could you replace with this implementation ? remove DO NOT MERGE and make it ready for review ?

Signed-off-by: Gabriel-Trintinalia <[email protected]>
@Gabriel-Trintinalia
Copy link
Contributor Author

Gabriel-Trintinalia commented Jul 4, 2025

@ahamlat @lu-pinto

The benchmarks show that when useBase32Wrapping is true, the results is much better in terms of performance. Which means that the fast implementation is this one

 numberOfLeadingZeros = value.numberOfLeadingZeros();
      if (value.size() != Bytes32.SIZE) {
        numberOfLeadingZeros += Bytes32.SIZE - value.size();
      }

Could you replace with this implementation ? remove DO NOT MERGE and make it ready for review ?

Unfortunately, It does not pass the reference tests. I added a failing test to the PR:

Arguments.of("0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff", 8));

The proposed change above returns 1.

Screenshot 2025-07-04 at 1 50 12 pm

@lu-pinto
Copy link
Member

lu-pinto commented Jul 4, 2025

@ahamlat @lu-pinto

The benchmarks show that when useBase32Wrapping is true, the results is much better in terms of performance. Which means that the fast implementation is this one

 numberOfLeadingZeros = value.numberOfLeadingZeros();
      if (value.size() != Bytes32.SIZE) {
        numberOfLeadingZeros += Bytes32.SIZE - value.size();
      }

Could you replace with this implementation ? remove DO NOT MERGE and make it ready for review ?

Unfortunately, It does not pass the reference tests. I added a failing test to the PR:

Arguments.of("0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff", 8));

The proposed change above returns 1.

Screenshot 2025-07-04 at 1 50 12 pm

Fixed on the new tip 5607b1c. Was partially counting bytes instead of bits :)

@ahamlat
Copy link
Contributor

ahamlat commented Jul 4, 2025

We had a chat about it with @lu-pinto . There's one main issue and the unit test is made in a way to reproduce the reference test :

  1. The main issue is :
 numberOfLeadingZeros += Bytes32.SIZE - value.size();

This should be

numberOfLeadingZeros += 8 * (Bytes32.SIZE - value.size());
  1. The second one is not really an issue, but it helped to show the issue in the unit test
    Instead of
Bytes input = Bytes.fromHexString(value);

You can use

Bytes input = Bytes32.fromHexString(value);

But may be we should keep using Bytes instead just to be sure we're handling correctly this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Osaka Osaka fork related - part of Fusaka
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

5 participants