From 60fdfe0c1ad115a2fba9394b7d011af343824fa7 Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Wed, 1 Apr 2026 16:07:37 +0200 Subject: [PATCH 1/9] Add SHL, SHR and SAR implementations and benchmarks for EVM v2 Signed-off-by: Ameziane H. --- .../v2/AbstractSarOperationBenchmarkV2.java | 153 +++++++++++ .../v2/AbstractShiftOperationBenchmarkV2.java | 120 +++++++++ .../v2/AddOperationBenchmarkV2.java | 2 +- .../vm/operations/v2/BenchmarkHelperV2.java | 41 +++ .../v2/SarOperationBenchmarkV2.java | 27 ++ .../v2/ShlOperationBenchmarkV2.java | 27 ++ .../v2/ShrOperationBenchmarkV2.java | 27 ++ .../java/org/hyperledger/besu/evm/EVM.java | 17 +- .../besu/evm/v2/StackArithmetic.java | 255 ++++++++++++++++++ .../AbstractFixedCostOperationV2.java | 2 +- .../v2 => v2/operation}/AddOperationV2.java | 2 +- .../besu/evm/v2/operation/SarOperationV2.java | 55 ++++ .../besu/evm/v2/operation/ShlOperationV2.java | 55 ++++ .../besu/evm/v2/operation/ShrOperationV2.java | 55 ++++ 14 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java create mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java rename evm/src/main/java/org/hyperledger/besu/evm/{operation/v2 => v2/operation}/AbstractFixedCostOperationV2.java (98%) rename evm/src/main/java/org/hyperledger/besu/evm/{operation/v2 => v2/operation}/AddOperationV2.java (97%) create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java create mode 100644 evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java new file mode 100644 index 00000000000..215e75adb1d --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java @@ -0,0 +1,153 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations.v2; + +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomNegativeUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomPositiveUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; + +import org.hyperledger.besu.evm.UInt256; + +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + +/** + * Abstract base class for SAR (Shift Arithmetic Right) operation benchmarks. + * + *

SAR has additional test cases for negative/positive values to test sign extension behavior. + */ +public abstract class AbstractSarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + /** Test cases covering different execution paths for SAR operations. */ + public enum Case { + /** Shift by 0 - early return path. */ + SHIFT_0, + /** Negative number (ALL_BITS) with shift=1 - tests sign extension OR path. */ + NEGATIVE_SHIFT_1, + /** value with all bits to 1 with shift=1 * */ + ALL_BITS_SHIFT_1, + /** Positive number with shift=1 - no sign extension needed. */ + POSITIVE_SHIFT_1, + /** Negative number with medium shift. */ + NEGATIVE_SHIFT_128, + /** Negative number with max shift. */ + NEGATIVE_SHIFT_255, + /** Positive number with medium shift. */ + POSITIVE_SHIFT_128, + /** positive number with max shift. */ + POSITIVE_SHIFT_255, + /** Overflow: shift >= 256. */ + OVERFLOW_SHIFT_256, + /** Overflow: shift amount > 4 bytes. */ + OVERFLOW_LARGE_SHIFT, + /** Random values (original behavior). */ + FULL_RANDOM + } + + @Param({ + "SHIFT_0", + "NEGATIVE_SHIFT_1", + "POSITIVE_SHIFT_1", + "ALL_BITS_SHIFT_1", + "NEGATIVE_SHIFT_128", + "NEGATIVE_SHIFT_255", + "POSITIVE_SHIFT_128", + "POSITIVE_SHIFT_255", + "OVERFLOW_SHIFT_256", + "OVERFLOW_LARGE_SHIFT", + "FULL_RANDOM" + }) + protected String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelperV2.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new UInt256[SAMPLE_SIZE]; // shift amount (pushed second, popped first) + bPool = new UInt256[SAMPLE_SIZE]; // value (pushed first, popped second) + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < SAMPLE_SIZE; i++) { + switch (scenario) { + case SHIFT_0: + aPool[i] = UInt256.fromInt(0); + bPool[i] = randomUInt256Value(random); + break; + + case NEGATIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case ALL_BITS_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = UInt256.MAX; + break; + + case POSITIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case NEGATIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case NEGATIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case POSITIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomPositiveUInt256Value(random); + break; + case POSITIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = UInt256.fromInt(256); + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); + bPool[i] = randomUInt256Value(random); + break; + + case FULL_RANDOM: + default: + final byte[] shift = new byte[1 + random.nextInt(2)]; + final byte[] value = new byte[1 + random.nextInt(32)]; + random.nextBytes(shift); + random.nextBytes(value); + aPool[i] = bytesToUInt256(shift); + bPool[i] = bytesToUInt256(value); + break; + } + } + index = 0; + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java new file mode 100644 index 00000000000..cd83e28f46f --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java @@ -0,0 +1,120 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations.v2; + +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; + +import org.hyperledger.besu.evm.UInt256; + +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + +/** + * Abstract base class for shift operation benchmarks (SHL, SHR, SAR). + * + *

Provides shared test case definitions and setup logic. + */ +public abstract class AbstractShiftOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + /** Test cases covering different execution paths for shift operations. */ + public enum Case { + /** Shift by 0 - no shift needed. */ + SHIFT_0, + /** Small shift by 1 bit. */ + SHIFT_1, + /** Medium shift by 128 bits (half word). */ + SHIFT_128, + /** Large shift by 255 bits (max valid). */ + SHIFT_255, + /** Overflow: shift of exactly 256. */ + OVERFLOW_SHIFT_256, + /** Overflow: shift amount > 4 bytes. */ + OVERFLOW_LARGE_SHIFT, + /** Random values (original behavior). */ + FULL_RANDOM + } + + @Param({ + "SHIFT_0", + "SHIFT_1", + "SHIFT_128", + "SHIFT_255", + "OVERFLOW_SHIFT_256", + "OVERFLOW_LARGE_SHIFT", + "FULL_RANDOM" + }) + protected String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelperV2.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new UInt256[SAMPLE_SIZE]; // shift amount (pushed second, popped first) + bPool = new UInt256[SAMPLE_SIZE]; // value (pushed first, popped second) + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < SAMPLE_SIZE; i++) { + switch (scenario) { + case SHIFT_0: + aPool[i] = UInt256.ZERO; + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_1: + aPool[i] = UInt256.ONE; + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomUInt256Value(random); + break; + + case SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = UInt256.fromInt(256); // 256 + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); + bPool[i] = randomUInt256Value(random); + break; + + case FULL_RANDOM: + default: + final byte[] shift = new byte[1 + random.nextInt(4)]; + final byte[] value = new byte[1 + random.nextInt(32)]; + random.nextBytes(shift); + random.nextBytes(value); + aPool[i] = bytesToUInt256(shift); + bPool[i] = bytesToUInt256(value); + break; + } + } + index = 0; + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java index c9e16edbd0e..97b7fa76aed 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AddOperationBenchmarkV2.java @@ -16,7 +16,7 @@ import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.operation.Operation; -import org.hyperledger.besu.evm.operation.v2.AddOperationV2; +import org.hyperledger.besu.evm.v2.operation.AddOperationV2; public class AddOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java index fa062fd5fb0..8afcbb92194 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/BenchmarkHelperV2.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.apache.tuweni.bytes.Bytes32; @@ -101,4 +102,44 @@ static void pushUInt256(final MessageFrame frame, final org.hyperledger.besu.evm s[dst + 3] = value.u0(); frame.setTopV2(top + 1); } + + /** + * Generates a random UInt256 value. + * + * @param random thread-local random source + * @return random UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomUInt256Value(final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } + + /** + * Generates a random positive signed 256-bit UInt256 value (sign bit cleared). + * + * @param random thread-local random source + * @return random positive UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomPositiveUInt256Value( + final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] & 0x7F); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } + + /** + * Generates a random negative signed 256-bit UInt256 value (sign bit set). + * + * @param random thread-local random source + * @return random negative UInt256 value + */ + static org.hyperledger.besu.evm.UInt256 randomNegativeUInt256Value( + final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] | 0x80); + return org.hyperledger.besu.evm.UInt256.fromBytesBE(value); + } } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java new file mode 100644 index 00000000000..00b8e116dbe --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java @@ -0,0 +1,27 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations.v2; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.SarOperationV2; + +public class SarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return SarOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java new file mode 100644 index 00000000000..1c3fb3a2498 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java @@ -0,0 +1,27 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations.v2; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; + +public class ShlOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShlOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java new file mode 100644 index 00000000000..3df751052d5 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java @@ -0,0 +1,27 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.vm.operations.v2; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; + +public class ShrOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShrOperationV2.staticOperation(frame, frame.stackDataV2()); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java index 99c4eb8014b..4384c5e3320 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -86,8 +86,11 @@ import org.hyperledger.besu.evm.operation.VirtualOperation; import org.hyperledger.besu.evm.operation.XorOperation; import org.hyperledger.besu.evm.operation.XorOperationOptimized; -import org.hyperledger.besu.evm.operation.v2.AddOperationV2; import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.v2.operation.AddOperationV2; +import org.hyperledger.besu.evm.v2.operation.SarOperationV2; +import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; +import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; import java.util.Optional; import java.util.function.Function; @@ -485,6 +488,18 @@ private void runToHaltV2(final MessageFrame frame, final OperationTracer tracing result = switch (opcode) { case 0x01 -> AddOperationV2.staticOperation(frame, frame.stackDataV2()); + case 0x1b -> + enableConstantinople + ? ShlOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); + case 0x1c -> + enableConstantinople + ? ShrOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); + case 0x1d -> + enableConstantinople + ? SarOperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); // TODO: implement remaining opcodes in v2; until then fall through to v1 default -> { frame.setCurrentOperation(currentOperation); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java new file mode 100644 index 00000000000..a24ae1dbc3f --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -0,0 +1,255 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2; + +/** + * Static utility operating directly on the flat {@code long[]} operand stack. Each slot occupies 4 + * consecutive longs in big-endian limb order: {@code [u3, u2, u1, u0]} where u3 is the most + * significant limb. + * + *

All methods take {@code (long[] s, int top)} and return the new {@code top}. The caller + * (operation) is responsible for underflow/overflow checks before calling. + */ +public class StackArithmetic { + + /** SHL: s[top-2] = s[top-2] << s[top-1], return top-1. */ + public static int shl(final long[] s, final int top) { + final int a = (top - 1) << 2; // shift amount + final int b = (top - 2) << 2; // value + // If shift amount > 255 or value is zero, result is zero + if (s[a] != 0 + || s[a + 1] != 0 + || s[a + 2] != 0 + || Long.compareUnsigned(s[a + 3], 256) >= 0 + || (s[b] == 0 && s[b + 1] == 0 && s[b + 2] == 0 && s[b + 3] == 0)) { + s[b] = 0; + s[b + 1] = 0; + s[b + 2] = 0; + s[b + 3] = 0; + return top - 1; + } + int shift = (int) s[a + 3]; + shiftLeftInPlace(s, b, shift); + return top - 1; + } + + /** Shift left in place. shift must be 0..255. */ + private static void shiftLeftInPlace(final long[] s, final int off, final int shift) { + if (shift == 0) return; + int limbShift = shift >>> 6; + int bitShift = shift & 63; + + // Move limbs (stored as [u3, u2, u1, u0] at [off, off+1, off+2, off+3]) + // u3=off, u2=off+1, u1=off+2, u0=off+3 + long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; + long a0, a1, a2, a3; + switch (limbShift) { + case 0: + a0 = u0; + a1 = u1; + a2 = u2; + a3 = u3; + break; + case 1: + a0 = 0; + a1 = u0; + a2 = u1; + a3 = u2; + break; + case 2: + a0 = 0; + a1 = 0; + a2 = u0; + a3 = u1; + break; + case 3: + a0 = 0; + a1 = 0; + a2 = 0; + a3 = u0; + break; + default: + s[off] = 0; + s[off + 1] = 0; + s[off + 2] = 0; + s[off + 3] = 0; + return; + } + + if (bitShift == 0) { + s[off] = a3; + s[off + 1] = a2; + s[off + 2] = a1; + s[off + 3] = a0; + } else { + int inv = 64 - bitShift; + s[off + 3] = a0 << bitShift; + s[off + 2] = (a1 << bitShift) | (a0 >>> inv); + s[off + 1] = (a2 << bitShift) | (a1 >>> inv); + s[off] = (a3 << bitShift) | (a2 >>> inv); + } + } + + /** SHR: s[top-2] = s[top-2] >>> s[top-1], return top-1. */ + public static int shr(final long[] s, final int top) { + final int a = (top - 1) << 2; // shift amount + final int b = (top - 2) << 2; // value + if (s[a] != 0 + || s[a + 1] != 0 + || s[a + 2] != 0 + || Long.compareUnsigned(s[a + 3], 256) >= 0 + || (s[b] == 0 && s[b + 1] == 0 && s[b + 2] == 0 && s[b + 3] == 0)) { + s[b] = 0; + s[b + 1] = 0; + s[b + 2] = 0; + s[b + 3] = 0; + return top - 1; + } + int shift = (int) s[a + 3]; + shiftRightInPlace(s, b, shift); + return top - 1; + } + + /** Logical shift right in place. shift must be 0..255. */ + private static void shiftRightInPlace(final long[] s, final int off, final int shift) { + if (shift == 0) return; + int limbShift = shift >>> 6; + int bitShift = shift & 63; + + long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; + long a0, a1, a2, a3; + switch (limbShift) { + case 0: + a0 = u0; + a1 = u1; + a2 = u2; + a3 = u3; + break; + case 1: + a0 = u1; + a1 = u2; + a2 = u3; + a3 = 0; + break; + case 2: + a0 = u2; + a1 = u3; + a2 = 0; + a3 = 0; + break; + case 3: + a0 = u3; + a1 = 0; + a2 = 0; + a3 = 0; + break; + default: + s[off] = 0; + s[off + 1] = 0; + s[off + 2] = 0; + s[off + 3] = 0; + return; + } + + if (bitShift == 0) { + s[off] = a3; + s[off + 1] = a2; + s[off + 2] = a1; + s[off + 3] = a0; + } else { + int inv = 64 - bitShift; + s[off] = a3 >>> bitShift; + s[off + 1] = (a2 >>> bitShift) | (a3 << inv); + s[off + 2] = (a1 >>> bitShift) | (a2 << inv); + s[off + 3] = (a0 >>> bitShift) | (a1 << inv); + } + } + + /** SAR: s[top-2] = s[top-2] >> s[top-1] (arithmetic), return top-1. */ + public static int sar(final long[] s, final int top) { + final int a = (top - 1) << 2; // shift amount + final int b = (top - 2) << 2; // value + boolean negative = s[b] < 0; // MSB of u3 + + if (s[a] != 0 || s[a + 1] != 0 || s[a + 2] != 0 || Long.compareUnsigned(s[a + 3], 256) >= 0) { + long fill = negative ? -1L : 0L; + s[b] = fill; + s[b + 1] = fill; + s[b + 2] = fill; + s[b + 3] = fill; + return top - 1; + } + int shift = (int) s[a + 3]; + sarInPlace(s, b, shift, negative); + return top - 1; + } + + /** Arithmetic shift right in place. shift must be 0..255. */ + private static void sarInPlace( + final long[] s, final int off, final int shift, final boolean negative) { + if (shift == 0) return; + int limbShift = shift >>> 6; + int bitShift = shift & 63; + long fill = negative ? -1L : 0L; + + long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; + long a0, a1, a2, a3; + switch (limbShift) { + case 0: + a0 = u0; + a1 = u1; + a2 = u2; + a3 = u3; + break; + case 1: + a0 = u1; + a1 = u2; + a2 = u3; + a3 = fill; + break; + case 2: + a0 = u2; + a1 = u3; + a2 = fill; + a3 = fill; + break; + case 3: + a0 = u3; + a1 = fill; + a2 = fill; + a3 = fill; + break; + default: + s[off] = fill; + s[off + 1] = fill; + s[off + 2] = fill; + s[off + 3] = fill; + return; + } + + if (bitShift == 0) { + s[off] = a3; + s[off + 1] = a2; + s[off + 2] = a1; + s[off + 3] = a0; + } else { + int inv = 64 - bitShift; + s[off] = a3 >> bitShift; // arithmetic shift for MSB + s[off + 1] = (a2 >>> bitShift) | (a3 << inv); + s[off + 2] = (a1 >>> bitShift) | (a2 << inv); + s[off + 3] = (a0 >>> bitShift) | (a1 << inv); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java similarity index 98% rename from evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java rename to evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java index 95e059d9605..0480e77b067 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AbstractFixedCostOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.evm.operation.v2; +package org.hyperledger.besu.evm.v2.operation; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java similarity index 97% rename from evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java rename to evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java index 4c866bcf61f..b4109d29ad1 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/operation/v2/AddOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.evm.operation.v2; +package org.hyperledger.besu.evm.v2.operation; import org.hyperledger.besu.evm.EVM; import org.hyperledger.besu.evm.frame.MessageFrame; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java new file mode 100644 index 00000000000..0a84bca8526 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java @@ -0,0 +1,55 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Sar operation. */ +public class SarOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Sar operation success result. */ + static final OperationResult sarSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Sar operation. + * + * @param gasCalculator the gas calculator + */ + public SarOperationV2(final GasCalculator gasCalculator) { + super(0x1d, "SAR", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs sar operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.sar(s, frame.stackTopV2())); + return sarSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java new file mode 100644 index 00000000000..8aa84cab477 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java @@ -0,0 +1,55 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Shl (Shift Left) operation. */ +public class ShlOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Shl operation success result. */ + static final OperationResult shlSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Shl operation. + * + * @param gasCalculator the gas calculator + */ + public ShlOperationV2(final GasCalculator gasCalculator) { + super(0x1b, "SHL", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs Shift Left operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.shl(s, frame.stackTopV2())); + return shlSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java new file mode 100644 index 00000000000..feebb57e702 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java @@ -0,0 +1,55 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.v2.StackArithmetic; + +/** The Shr (Shift Right) operation. */ +public class ShrOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Shr operation success result. */ + static final OperationResult shrSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Shr operation. + * + * @param gasCalculator the gas calculator + */ + public ShrOperationV2(final GasCalculator gasCalculator) { + super(0x1c, "SHR", 2, 1, gasCalculator, gasCalculator.getVeryLowTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs SHR operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.shr(s, frame.stackTopV2())); + return shrSuccess; + } +} From 1dd435b8ceb0284651365488bbc4c2da592c23c6 Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Wed, 1 Apr 2026 16:16:49 +0200 Subject: [PATCH 2/9] Use more accurate benchmarks on shift operations Signed-off-by: Ameziane H. --- .../besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java | 2 +- .../besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java | 2 +- .../besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java index 00b8e116dbe..626b1912e62 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java @@ -18,7 +18,7 @@ import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.v2.operation.SarOperationV2; -public class SarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { +public class SarOperationBenchmarkV2 extends AbstractSarOperationBenchmarkV2 { @Override protected Operation.OperationResult invoke(final MessageFrame frame) { diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java index 1c3fb3a2498..117f8148036 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShlOperationBenchmarkV2.java @@ -18,7 +18,7 @@ import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; -public class ShlOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { +public class ShlOperationBenchmarkV2 extends AbstractShiftOperationBenchmarkV2 { @Override protected Operation.OperationResult invoke(final MessageFrame frame) { diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java index 3df751052d5..fa3dd952609 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/ShrOperationBenchmarkV2.java @@ -18,7 +18,7 @@ import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; -public class ShrOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { +public class ShrOperationBenchmarkV2 extends AbstractShiftOperationBenchmarkV2 { @Override protected Operation.OperationResult invoke(final MessageFrame frame) { From 45f61e5e2f96475c6c3a3f1783e29123ddd60335 Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 15:18:39 +0200 Subject: [PATCH 3/9] Use existing implementation on SAR, SHR and SHL Signed-off-by: Ameziane H. --- .../besu/evm/v2/StackArithmetic.java | 387 ++++++++++-------- .../besu/evm/v2/operation/SarOperationV2.java | 4 +- 2 files changed, 216 insertions(+), 175 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java index a24ae1dbc3f..514307e09e2 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -24,232 +24,273 @@ */ public class StackArithmetic { - /** SHL: s[top-2] = s[top-2] << s[top-1], return top-1. */ - public static int shl(final long[] s, final int top) { - final int a = (top - 1) << 2; // shift amount - final int b = (top - 2) << 2; // value + /** + * Performs EVM SHL (shift left) on the two top stack items. + * + *

Pops the shift amount (unsigned) and the value, pushes {@code value << shift}. Shifts >= 256 + * or a zero value produce 0. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int shl(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; // If shift amount > 255 or value is zero, result is zero - if (s[a] != 0 - || s[a + 1] != 0 - || s[a + 2] != 0 - || Long.compareUnsigned(s[a + 3], 256) >= 0 - || (s[b] == 0 && s[b + 1] == 0 && s[b + 2] == 0 && s[b + 3] == 0)) { - s[b] = 0; - s[b + 1] = 0; - s[b + 2] = 0; - s[b + 3] = 0; + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0 + || (stack[valueOffset] == 0 + && stack[valueOffset + 1] == 0 + && stack[valueOffset + 2] == 0 + && stack[valueOffset + 3] == 0)) { + stack[valueOffset] = 0; + stack[valueOffset + 1] = 0; + stack[valueOffset + 2] = 0; + stack[valueOffset + 3] = 0; return top - 1; } - int shift = (int) s[a + 3]; - shiftLeftInPlace(s, b, shift); + int shift = (int) stack[shiftOffset + 3]; + shiftLeftInPlace(stack, valueOffset, shift); return top - 1; } - /** Shift left in place. shift must be 0..255. */ - private static void shiftLeftInPlace(final long[] s, final int off, final int shift) { + /** + * Left-shifts a 256-bit value in place by 1..255 bits, zero-filling from the right. + * + * @param stack the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [1, 255]) + */ + private static void shiftLeftInPlace(final long[] stack, final int valueOffset, final int shift) { if (shift == 0) return; - int limbShift = shift >>> 6; - int bitShift = shift & 63; - - // Move limbs (stored as [u3, u2, u1, u0] at [off, off+1, off+2, off+3]) - // u3=off, u2=off+1, u1=off+2, u0=off+3 - long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; - long a0, a1, a2, a3; - switch (limbShift) { + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + final int wordShift = shift >>> 6; + final int bitShift = shift & 63; + switch (wordShift) { case 0: - a0 = u0; - a1 = u1; - a2 = u2; - a3 = u3; + w0 = shiftLeftWord(w0, w1, bitShift); + w1 = shiftLeftWord(w1, w2, bitShift); + w2 = shiftLeftWord(w2, w3, bitShift); + w3 = shiftLeftWord(w3, 0, bitShift); break; case 1: - a0 = 0; - a1 = u0; - a2 = u1; - a3 = u2; + w0 = shiftLeftWord(w1, w2, bitShift); + w1 = shiftLeftWord(w2, w3, bitShift); + w2 = shiftLeftWord(w3, 0, bitShift); + w3 = 0; break; case 2: - a0 = 0; - a1 = 0; - a2 = u0; - a3 = u1; + w0 = shiftLeftWord(w2, w3, bitShift); + w1 = shiftLeftWord(w3, 0, bitShift); + w2 = 0; + w3 = 0; break; case 3: - a0 = 0; - a1 = 0; - a2 = 0; - a3 = u0; + w0 = shiftLeftWord(w3, 0, bitShift); + w1 = 0; + w2 = 0; + w3 = 0; break; - default: - s[off] = 0; - s[off + 1] = 0; - s[off + 2] = 0; - s[off + 3] = 0; - return; } + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; + } - if (bitShift == 0) { - s[off] = a3; - s[off + 1] = a2; - s[off + 2] = a1; - s[off + 3] = a0; - } else { - int inv = 64 - bitShift; - s[off + 3] = a0 << bitShift; - s[off + 2] = (a1 << bitShift) | (a0 >>> inv); - s[off + 1] = (a2 << bitShift) | (a1 >>> inv); - s[off] = (a3 << bitShift) | (a2 >>> inv); - } + /** + * Shifts a 64-bit word left and carries in bits from the next less-significant word. + * + * @param value the current word + * @param nextValue the next less-significant word (bits carry in from its top) + * @param bitShift the intra-word shift amount in [0, 63]; 0 returns {@code value} unchanged to + * avoid Java's mod-64 shift semantics on {@code nextValue >>> 64} + * @return the shifted word + */ + private static long shiftLeftWord(final long value, final long nextValue, final int bitShift) { + if (bitShift == 0) return value; + return (value << bitShift) | (nextValue >>> (64 - bitShift)); } - /** SHR: s[top-2] = s[top-2] >>> s[top-1], return top-1. */ - public static int shr(final long[] s, final int top) { - final int a = (top - 1) << 2; // shift amount - final int b = (top - 2) << 2; // value - if (s[a] != 0 - || s[a + 1] != 0 - || s[a + 2] != 0 - || Long.compareUnsigned(s[a + 3], 256) >= 0 - || (s[b] == 0 && s[b + 1] == 0 && s[b + 2] == 0 && s[b + 3] == 0)) { - s[b] = 0; - s[b + 1] = 0; - s[b + 2] = 0; - s[b + 3] = 0; + /** + * Performs EVM SHR (logical shift right) on the two top stack items. + * + *

Pops the shift amount (unsigned) and the value, pushes {@code value >>> shift}. Shifts >= + * 256 or a zero value produce 0. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int shr(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0 + || (stack[valueOffset] == 0 + && stack[valueOffset + 1] == 0 + && stack[valueOffset + 2] == 0 + && stack[valueOffset + 3] == 0)) { + stack[valueOffset] = 0; + stack[valueOffset + 1] = 0; + stack[valueOffset + 2] = 0; + stack[valueOffset + 3] = 0; return top - 1; } - int shift = (int) s[a + 3]; - shiftRightInPlace(s, b, shift); + int shift = (int) stack[shiftOffset + 3]; + shiftRightInPlace(stack, valueOffset, shift); return top - 1; } - /** Logical shift right in place. shift must be 0..255. */ - private static void shiftRightInPlace(final long[] s, final int off, final int shift) { + /** + * Logically right-shifts a 256-bit value in place by 1..255 bits, zero-filling from the left. + * + * @param s the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [1, 255]) + */ + private static void shiftRightInPlace(final long[] s, final int valueOffset, final int shift) { if (shift == 0) return; - int limbShift = shift >>> 6; - int bitShift = shift & 63; - - long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; - long a0, a1, a2, a3; - switch (limbShift) { + long w0 = s[valueOffset], + w1 = s[valueOffset + 1], + w2 = s[valueOffset + 2], + w3 = s[valueOffset + 3]; + final int wordShift = shift >>> 6; + final int bitShift = shift & 63; + switch (wordShift) { case 0: - a0 = u0; - a1 = u1; - a2 = u2; - a3 = u3; + w3 = shiftRightWord(w3, w2, bitShift); + w2 = shiftRightWord(w2, w1, bitShift); + w1 = shiftRightWord(w1, w0, bitShift); + w0 = shiftRightWord(w0, 0, bitShift); break; case 1: - a0 = u1; - a1 = u2; - a2 = u3; - a3 = 0; + w3 = shiftRightWord(w2, w1, bitShift); + w2 = shiftRightWord(w1, w0, bitShift); + w1 = shiftRightWord(w0, 0, bitShift); + w0 = 0; break; case 2: - a0 = u2; - a1 = u3; - a2 = 0; - a3 = 0; + w3 = shiftRightWord(w1, w0, bitShift); + w2 = shiftRightWord(w0, 0, bitShift); + w1 = 0; + w0 = 0; break; case 3: - a0 = u3; - a1 = 0; - a2 = 0; - a3 = 0; + w3 = shiftRightWord(w0, 0, bitShift); + w2 = 0; + w1 = 0; + w0 = 0; break; - default: - s[off] = 0; - s[off + 1] = 0; - s[off + 2] = 0; - s[off + 3] = 0; - return; - } - - if (bitShift == 0) { - s[off] = a3; - s[off + 1] = a2; - s[off + 2] = a1; - s[off + 3] = a0; - } else { - int inv = 64 - bitShift; - s[off] = a3 >>> bitShift; - s[off + 1] = (a2 >>> bitShift) | (a3 << inv); - s[off + 2] = (a1 >>> bitShift) | (a2 << inv); - s[off + 3] = (a0 >>> bitShift) | (a1 << inv); } + s[valueOffset] = w0; + s[valueOffset + 1] = w1; + s[valueOffset + 2] = w2; + s[valueOffset + 3] = w3; } - /** SAR: s[top-2] = s[top-2] >> s[top-1] (arithmetic), return top-1. */ - public static int sar(final long[] s, final int top) { - final int a = (top - 1) << 2; // shift amount - final int b = (top - 2) << 2; // value - boolean negative = s[b] < 0; // MSB of u3 - - if (s[a] != 0 || s[a + 1] != 0 || s[a + 2] != 0 || Long.compareUnsigned(s[a + 3], 256) >= 0) { + /** + * Performs EVM SAR (arithmetic shift right) on the two top stack items. + * + *

Pops the shift amount (unsigned) and the value (signed), pushes {@code value >> shift}. + * Shifts >= 256 produce 0 for positive values and -1 for negative values. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top after consuming one item + */ + public static int sar(final long[] stack, final int top) { + final int shiftOffset = (top - 1) << 2; + final int valueOffset = (top - 2) << 2; + boolean negative = stack[valueOffset] < 0; + if (stack[shiftOffset] != 0 + || stack[shiftOffset + 1] != 0 + || stack[shiftOffset + 2] != 0 + || Long.compareUnsigned(stack[shiftOffset + 3], 256) >= 0) { long fill = negative ? -1L : 0L; - s[b] = fill; - s[b + 1] = fill; - s[b + 2] = fill; - s[b + 3] = fill; + stack[valueOffset] = fill; + stack[valueOffset + 1] = fill; + stack[valueOffset + 2] = fill; + stack[valueOffset + 3] = fill; return top - 1; } - int shift = (int) s[a + 3]; - sarInPlace(s, b, shift, negative); + int shift = (int) stack[shiftOffset + 3]; + sarInPlace(stack, valueOffset, shift, negative); return top - 1; } - /** Arithmetic shift right in place. shift must be 0..255. */ + /** + * Arithmetic right-shifts a 256-bit value in place by 0..255 bits, sign-extending with {@code + * fill}. + * + * @param stack the flat limb array + * @param valueOffset index of the value's most-significant limb + * @param shift number of bits to shift (must be in [0, 255]) + * @param negative true if the original value is negative (fill = -1) + */ private static void sarInPlace( - final long[] s, final int off, final int shift, final boolean negative) { + final long[] stack, final int valueOffset, final int shift, final boolean negative) { if (shift == 0) return; - int limbShift = shift >>> 6; - int bitShift = shift & 63; - long fill = negative ? -1L : 0L; - - long u0 = s[off + 3], u1 = s[off + 2], u2 = s[off + 1], u3 = s[off]; - long a0, a1, a2, a3; - switch (limbShift) { + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + final long fill = negative ? -1L : 0L; + final int wordShift = shift >>> 6; + final int bitShift = shift & 63; + switch (wordShift) { case 0: - a0 = u0; - a1 = u1; - a2 = u2; - a3 = u3; + w3 = shiftRightWord(w3, w2, bitShift); + w2 = shiftRightWord(w2, w1, bitShift); + w1 = shiftRightWord(w1, w0, bitShift); + w0 = shiftRightWord(w0, fill, bitShift); break; case 1: - a0 = u1; - a1 = u2; - a2 = u3; - a3 = fill; + w3 = shiftRightWord(w2, w1, bitShift); + w2 = shiftRightWord(w1, w0, bitShift); + w1 = shiftRightWord(w0, fill, bitShift); + w0 = fill; break; case 2: - a0 = u2; - a1 = u3; - a2 = fill; - a3 = fill; + w3 = shiftRightWord(w1, w0, bitShift); + w2 = shiftRightWord(w0, fill, bitShift); + w1 = fill; + w0 = fill; break; case 3: - a0 = u3; - a1 = fill; - a2 = fill; - a3 = fill; + w3 = shiftRightWord(w0, fill, bitShift); + w2 = fill; + w1 = fill; + w0 = fill; break; - default: - s[off] = fill; - s[off + 1] = fill; - s[off + 2] = fill; - s[off + 3] = fill; - return; } + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; + } - if (bitShift == 0) { - s[off] = a3; - s[off + 1] = a2; - s[off + 2] = a1; - s[off + 3] = a0; - } else { - int inv = 64 - bitShift; - s[off] = a3 >> bitShift; // arithmetic shift for MSB - s[off + 1] = (a2 >>> bitShift) | (a3 << inv); - s[off + 2] = (a1 >>> bitShift) | (a2 << inv); - s[off + 3] = (a0 >>> bitShift) | (a1 << inv); - } + /** + * Shifts a 64-bit word right and carries in bits from the previous more-significant word. + * + *

The {@code bitShift == 0} fast path avoids Java long-shift masking, where a shift by 64 is + * treated as a shift by 0. + * + * @param value the current word + * @param prevValue the previous more-significant word + * @param bitShift the intra-word shift amount in the range {@code [0..63]} + * @return the shifted word + */ + private static long shiftRightWord(final long value, final long prevValue, final int bitShift) { + if (bitShift == 0) return value; + return (value >>> bitShift) | (prevValue << (64 - bitShift)); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java index 0a84bca8526..4a3dafc1b1f 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java @@ -47,9 +47,9 @@ public Operation.OperationResult executeFixedCostOperation( * @param frame the frame * @return the operation result */ - public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; - frame.setTopV2(StackArithmetic.sar(s, frame.stackTopV2())); + frame.setTopV2(StackArithmetic.sar(stack, frame.stackTopV2())); return sarSuccess; } } From 531a87be06cacbdd5affac0506e9f1a4f99cf2f2 Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 16:43:37 +0200 Subject: [PATCH 4/9] Add unit tests. Signed-off-by: Ameziane H. --- .../evm/v2/operation/SarOperationV2Test.java | 196 ++++++++++++++++++ .../evm/v2/operation/ShlOperationV2Test.java | 132 ++++++++++++ .../evm/v2/operation/ShrOperationV2Test.java | 144 +++++++++++++ .../testutils/TestMessageFrameBuilderV2.java | 190 +++++++++++++++++ 4 files changed, 662 insertions(+) create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java new file mode 100644 index 00000000000..528b9515782 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2Test.java @@ -0,0 +1,196 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SarOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final SarOperationV2 operation = new SarOperationV2(gasCalculator); + + static Iterable data() { + return List.of( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0xc000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x101", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x4000000000000000000000000000000000000000000000000000000000000000", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xf8", + "0x000000000000000000000000000000000000000000000000000000000000007f"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x100", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0xffffffffffffffffffffffffffffffff80000000000000000000000000000000"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); + } + + @ParameterizedTest(name = "{index}: {0}, {1}, {2}") + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + @Test + void dryRunDetector() { + assertThat(true) + .withFailMessage("This test is here so gradle --dry-run executes this class") + .isTrue(); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java new file mode 100644 index 00000000000..246c4198201 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2Test.java @@ -0,0 +1,132 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ShlOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShlOperationV2 operation = new ShlOperationV2(gasCalculator); + + static Iterable data() { + return Arrays.asList( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000008"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x000000000000000000000000000000000000000000000000000000000000001e"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000010"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", "0x100", "0x"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000040000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x")); + } + + @ParameterizedTest + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + @Test + void dryRunDetector() { + assertThat(true) + .withFailMessage("This test is here so gradle --dry-run executes this class") + .isTrue(); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java new file mode 100644 index 00000000000..65949ff7a27 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2Test.java @@ -0,0 +1,144 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ShrOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShrOperationV2 operation = new ShrOperationV2(gasCalculator); + + static Iterable data() { + return Arrays.asList( + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002"), + Arguments.of( + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", "0x100", "0x"), + Arguments.of( + "0x8000000000000000000000000000000000000000000000000000000000000000", "0x101", "0x"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001"), + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x100", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80", + "0x0000000000000000000000000000000000000000000000000000000000000000"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", "0x8000", "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x80000000000000000000000000000000", + "0x"), + Arguments.of( + "0x0000000000000000000000000000000000000000000000000000000000000400", + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x")); + } + + @ParameterizedTest + @MethodSource("data") + void shiftOperation(final String number, final String shift, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient(number)) + .pushStackItem(Bytes32.fromHexStringLenient(shift)) + .build(); + operation.execute(frame, null); + UInt256 expected; + if (expectedResult.equals("0x") || expectedResult.equals("0x0")) { + expected = UInt256.ZERO; + } else { + expected = UInt256.fromBytesBE(Bytes32.fromHexStringLenient(expectedResult).toArrayUnsafe()); + } + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + @Test + void dryRunDetector() { + assertThat(true) + .withFailMessage("This test is here so gradle --dry-run executes this class") + .isTrue(); + } + + private static UInt256 getV2StackItem(final MessageFrame frame, final int offset) { + final long[] s = frame.stackDataV2(); + final int idx = (frame.stackTopV2() - 1 - offset) << 2; + return new UInt256(s[idx], s[idx + 1], s[idx + 2], s[idx + 3]); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java new file mode 100644 index 00000000000..ebf852e768a --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/testutils/TestMessageFrameBuilderV2.java @@ -0,0 +1,190 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.testutils; + +import static org.hyperledger.besu.evm.frame.MessageFrame.DEFAULT_MAX_STACK_SIZE; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.Code; +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.blockhash.BlockHashLookup; +import org.hyperledger.besu.evm.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.internal.Words; +import org.hyperledger.besu.evm.testutils.FakeBlockValues; +import org.hyperledger.besu.evm.toy.ToyWorld; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; + +public class TestMessageFrameBuilderV2 { + + public static final Address DEFAULT_ADDRESS = Address.fromHexString("0xe8f1b89"); + private static final int maxStackSize = DEFAULT_MAX_STACK_SIZE; + + private Optional blockValues = Optional.empty(); + private Optional worldUpdater = Optional.empty(); + private long initialGas = Long.MAX_VALUE; + private Address address = DEFAULT_ADDRESS; + private Address sender = DEFAULT_ADDRESS; + private Address originator = DEFAULT_ADDRESS; + private Address contract = DEFAULT_ADDRESS; + private Wei gasPrice = Wei.ZERO; + private Wei blobGasPrice = Wei.ZERO; + private Wei value = Wei.ZERO; + private Bytes inputData = Bytes.EMPTY; + private Code code = Code.EMPTY_CODE; + private int pc = 0; + private final List stackItems = new ArrayList<>(); + private Optional blockHashLookup = Optional.empty(); + private Bytes memory = Bytes.EMPTY; + private boolean isStatic = false; + + public TestMessageFrameBuilderV2 worldUpdater(final WorldUpdater worldUpdater) { + this.worldUpdater = Optional.of(worldUpdater); + return this; + } + + public TestMessageFrameBuilderV2 initialGas(final long initialGas) { + this.initialGas = initialGas; + return this; + } + + public TestMessageFrameBuilderV2 sender(final Address sender) { + this.sender = sender; + return this; + } + + public TestMessageFrameBuilderV2 address(final Address address) { + this.address = address; + return this; + } + + TestMessageFrameBuilderV2 originator(final Address originator) { + this.originator = originator; + return this; + } + + public TestMessageFrameBuilderV2 contract(final Address contract) { + this.contract = contract; + return this; + } + + public TestMessageFrameBuilderV2 gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; + return this; + } + + public TestMessageFrameBuilderV2 blobGasPrice(final Wei blobGasPrice) { + this.blobGasPrice = blobGasPrice; + return this; + } + + public TestMessageFrameBuilderV2 value(final Wei value) { + this.value = value; + return this; + } + + public TestMessageFrameBuilderV2 inputData(final Bytes inputData) { + this.inputData = inputData; + return this; + } + + public TestMessageFrameBuilderV2 code(final Code code) { + this.code = code; + return this; + } + + public TestMessageFrameBuilderV2 pc(final int pc) { + this.pc = pc; + return this; + } + + public TestMessageFrameBuilderV2 blockValues(final BlockValues blockValues) { + this.blockValues = Optional.of(blockValues); + return this; + } + + public TestMessageFrameBuilderV2 pushStackItem(final Bytes item) { + stackItems.add(item); + return this; + } + + public TestMessageFrameBuilderV2 blockHashLookup(final BlockHashLookup blockHashLookup) { + this.blockHashLookup = Optional.of(blockHashLookup); + return this; + } + + public TestMessageFrameBuilderV2 memory(final Bytes memory) { + this.memory = memory; + return this; + } + + public TestMessageFrameBuilderV2 isStatic(final boolean isStatic) { + this.isStatic = isStatic; + return this; + } + + public MessageFrame build() { + final MessageFrame frame = + MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .worldUpdater(worldUpdater.orElseGet(this::createDefaultWorldUpdater)) + .initialGas(initialGas) + .address(address) + .originator(originator) + .gasPrice(gasPrice) + .blobGasPrice(blobGasPrice) + .inputData(inputData) + .sender(sender) + .value(value) + .apparentValue(value) + .contract(contract) + .code(code) + .blockValues(blockValues.orElseGet(() -> new FakeBlockValues(1337))) + .completer(c -> {}) + .miningBeneficiary(Address.ZERO) + .blockHashLookup( + blockHashLookup.orElse((__, number) -> Hash.hash(Words.longBytes(number)))) + .maxStackSize(maxStackSize) + .isStatic(isStatic) + .enableEvmV2(true) + .build(); + frame.setPC(pc); + stackItems.forEach( + item -> { + final UInt256 val = UInt256.fromBytesBE(item.toArrayUnsafe()); + final long[] s = frame.stackDataV2(); + final int dst = frame.stackTopV2() << 2; + s[dst] = val.u3(); + s[dst + 1] = val.u2(); + s[dst + 2] = val.u1(); + s[dst + 3] = val.u0(); + frame.setTopV2(frame.stackTopV2() + 1); + }); + frame.writeMemory(0, memory.size(), memory); + return frame; + } + + private WorldUpdater createDefaultWorldUpdater() { + return new ToyWorld(); + } +} From 9b88b6d27686243268af0a08523a8845ae161cfd Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 16:50:50 +0200 Subject: [PATCH 5/9] Address review comments. Signed-off-by: Ameziane H. --- .../v2/AbstractSarOperationBenchmarkV2.java | 153 ------------------ .../v2/SarOperationBenchmarkV2.java | 131 ++++++++++++++- .../besu/evm/v2/StackArithmetic.java | 16 ++ 3 files changed, 146 insertions(+), 154 deletions(-) delete mode 100644 ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java deleted file mode 100644 index 215e75adb1d..00000000000 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractSarOperationBenchmarkV2.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright contributors to Besu. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.ethereum.vm.operations.v2; - -import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; -import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomNegativeUInt256Value; -import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomPositiveUInt256Value; -import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; - -import org.hyperledger.besu.evm.UInt256; - -import java.util.concurrent.ThreadLocalRandom; - -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Setup; - -/** - * Abstract base class for SAR (Shift Arithmetic Right) operation benchmarks. - * - *

SAR has additional test cases for negative/positive values to test sign extension behavior. - */ -public abstract class AbstractSarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { - - /** Test cases covering different execution paths for SAR operations. */ - public enum Case { - /** Shift by 0 - early return path. */ - SHIFT_0, - /** Negative number (ALL_BITS) with shift=1 - tests sign extension OR path. */ - NEGATIVE_SHIFT_1, - /** value with all bits to 1 with shift=1 * */ - ALL_BITS_SHIFT_1, - /** Positive number with shift=1 - no sign extension needed. */ - POSITIVE_SHIFT_1, - /** Negative number with medium shift. */ - NEGATIVE_SHIFT_128, - /** Negative number with max shift. */ - NEGATIVE_SHIFT_255, - /** Positive number with medium shift. */ - POSITIVE_SHIFT_128, - /** positive number with max shift. */ - POSITIVE_SHIFT_255, - /** Overflow: shift >= 256. */ - OVERFLOW_SHIFT_256, - /** Overflow: shift amount > 4 bytes. */ - OVERFLOW_LARGE_SHIFT, - /** Random values (original behavior). */ - FULL_RANDOM - } - - @Param({ - "SHIFT_0", - "NEGATIVE_SHIFT_1", - "POSITIVE_SHIFT_1", - "ALL_BITS_SHIFT_1", - "NEGATIVE_SHIFT_128", - "NEGATIVE_SHIFT_255", - "POSITIVE_SHIFT_128", - "POSITIVE_SHIFT_255", - "OVERFLOW_SHIFT_256", - "OVERFLOW_LARGE_SHIFT", - "FULL_RANDOM" - }) - protected String caseName; - - @Setup(Level.Iteration) - @Override - public void setUp() { - frame = BenchmarkHelperV2.createMessageCallFrame(); - - final Case scenario = Case.valueOf(caseName); - aPool = new UInt256[SAMPLE_SIZE]; // shift amount (pushed second, popped first) - bPool = new UInt256[SAMPLE_SIZE]; // value (pushed first, popped second) - - final ThreadLocalRandom random = ThreadLocalRandom.current(); - - for (int i = 0; i < SAMPLE_SIZE; i++) { - switch (scenario) { - case SHIFT_0: - aPool[i] = UInt256.fromInt(0); - bPool[i] = randomUInt256Value(random); - break; - - case NEGATIVE_SHIFT_1: - aPool[i] = UInt256.fromInt(1); - bPool[i] = randomNegativeUInt256Value(random); - break; - - case ALL_BITS_SHIFT_1: - aPool[i] = UInt256.fromInt(1); - bPool[i] = UInt256.MAX; - break; - - case POSITIVE_SHIFT_1: - aPool[i] = UInt256.fromInt(1); - bPool[i] = randomPositiveUInt256Value(random); - break; - - case NEGATIVE_SHIFT_128: - aPool[i] = UInt256.fromInt(128); - bPool[i] = randomNegativeUInt256Value(random); - break; - - case NEGATIVE_SHIFT_255: - aPool[i] = UInt256.fromInt(255); - bPool[i] = randomNegativeUInt256Value(random); - break; - - case POSITIVE_SHIFT_128: - aPool[i] = UInt256.fromInt(128); - bPool[i] = randomPositiveUInt256Value(random); - break; - case POSITIVE_SHIFT_255: - aPool[i] = UInt256.fromInt(255); - bPool[i] = randomPositiveUInt256Value(random); - break; - - case OVERFLOW_SHIFT_256: - aPool[i] = UInt256.fromInt(256); - bPool[i] = randomUInt256Value(random); - break; - - case OVERFLOW_LARGE_SHIFT: - aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); - bPool[i] = randomUInt256Value(random); - break; - - case FULL_RANDOM: - default: - final byte[] shift = new byte[1 + random.nextInt(2)]; - final byte[] value = new byte[1 + random.nextInt(32)]; - random.nextBytes(shift); - random.nextBytes(value); - aPool[i] = bytesToUInt256(shift); - bPool[i] = bytesToUInt256(value); - break; - } - } - index = 0; - } -} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java index 626b1912e62..e617026a236 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java @@ -14,11 +14,140 @@ */ package org.hyperledger.besu.ethereum.vm.operations.v2; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.bytesToUInt256; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomNegativeUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomPositiveUInt256Value; +import static org.hyperledger.besu.ethereum.vm.operations.v2.BenchmarkHelperV2.randomUInt256Value; + +import org.hyperledger.besu.evm.UInt256; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.v2.operation.SarOperationV2; -public class SarOperationBenchmarkV2 extends AbstractSarOperationBenchmarkV2 { +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Setup; + +public class SarOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { + + /** Test cases covering different execution paths for SAR operations. */ + public enum Case { + /** Shift by 0 - early return path. */ + SHIFT_0, + /** Negative number with shift=1 - tests sign extension. */ + NEGATIVE_SHIFT_1, + /** Value with all bits set with shift=1. */ + ALL_BITS_SHIFT_1, + /** Positive number with shift=1 - no sign extension needed. */ + POSITIVE_SHIFT_1, + /** Negative number with medium shift. */ + NEGATIVE_SHIFT_128, + /** Negative number with max shift. */ + NEGATIVE_SHIFT_255, + /** Positive number with medium shift. */ + POSITIVE_SHIFT_128, + /** Positive number with max shift. */ + POSITIVE_SHIFT_255, + /** Overflow: shift >= 256. */ + OVERFLOW_SHIFT_256, + /** Overflow: shift amount > 4 bytes. */ + OVERFLOW_LARGE_SHIFT, + /** Random values. */ + FULL_RANDOM + } + + @Param({ + "SHIFT_0", + "NEGATIVE_SHIFT_1", + "POSITIVE_SHIFT_1", + "ALL_BITS_SHIFT_1", + "NEGATIVE_SHIFT_128", + "NEGATIVE_SHIFT_255", + "POSITIVE_SHIFT_128", + "POSITIVE_SHIFT_255", + "OVERFLOW_SHIFT_256", + "OVERFLOW_LARGE_SHIFT", + "FULL_RANDOM" + }) + protected String caseName; + + @Setup(Level.Iteration) + @Override + public void setUp() { + frame = BenchmarkHelperV2.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new UInt256[SAMPLE_SIZE]; + bPool = new UInt256[SAMPLE_SIZE]; + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < SAMPLE_SIZE; i++) { + switch (scenario) { + case SHIFT_0: + aPool[i] = UInt256.fromInt(0); + bPool[i] = randomUInt256Value(random); + break; + + case NEGATIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case ALL_BITS_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = UInt256.MAX; + break; + + case POSITIVE_SHIFT_1: + aPool[i] = UInt256.fromInt(1); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case NEGATIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case NEGATIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomNegativeUInt256Value(random); + break; + + case POSITIVE_SHIFT_128: + aPool[i] = UInt256.fromInt(128); + bPool[i] = randomPositiveUInt256Value(random); + break; + case POSITIVE_SHIFT_255: + aPool[i] = UInt256.fromInt(255); + bPool[i] = randomPositiveUInt256Value(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = UInt256.fromInt(256); + bPool[i] = randomUInt256Value(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = bytesToUInt256(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00}); + bPool[i] = randomUInt256Value(random); + break; + + case FULL_RANDOM: + default: + final byte[] shift = new byte[1 + random.nextInt(2)]; + final byte[] value = new byte[1 + random.nextInt(32)]; + random.nextBytes(shift); + random.nextBytes(value); + aPool[i] = bytesToUInt256(shift); + bPool[i] = bytesToUInt256(value); + break; + } + } + index = 0; + } @Override protected Operation.OperationResult invoke(final MessageFrame frame) { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java index 514307e09e2..7c7817d9d0e 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -24,6 +24,9 @@ */ public class StackArithmetic { + // region SHL (Shift Left) + // --------------------------------------------------------------------------- + /** * Performs EVM SHL (shift left) on the two top stack items. * @@ -118,6 +121,11 @@ private static long shiftLeftWord(final long value, final long nextValue, final return (value << bitShift) | (nextValue >>> (64 - bitShift)); } + // endregion + + // region SHR (Shift Right) + // --------------------------------------------------------------------------- + /** * Performs EVM SHR (logical shift right) on the two top stack items. * @@ -197,6 +205,11 @@ private static void shiftRightInPlace(final long[] s, final int valueOffset, fin s[valueOffset + 3] = w3; } + // endregion + + // region SAR (Shift Arithmetic Right) + // --------------------------------------------------------------------------- + /** * Performs EVM SAR (arithmetic shift right) on the two top stack items. * @@ -293,4 +306,7 @@ private static long shiftRightWord(final long value, final long prevValue, final if (bitShift == 0) return value; return (value >>> bitShift) | (prevValue << (64 - bitShift)); } + + // endregion + } From 189a3c84c9b6384942d205919318b403bad82b68 Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 17:28:12 +0200 Subject: [PATCH 6/9] Add property based testing for v2 shift operations Signed-off-by: Ameziane H. --- .../besu/evm/v2/StackArithmetic.java | 9 +- .../ShiftOperationsV2PropertyBasedTest.java | 592 ++++++++++++++++++ 2 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java index 7c7817d9d0e..a9685d4ffd6 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -24,6 +24,9 @@ */ public class StackArithmetic { + /** Utility class — not instantiable. */ + private StackArithmetic() {} + // region SHL (Shift Left) // --------------------------------------------------------------------------- @@ -291,6 +294,11 @@ private static void sarInPlace( stack[valueOffset + 3] = w3; } + // endregion + + // region Private Helpers + // --------------------------------------------------------------------------- + /** * Shifts a 64-bit word right and carries in bits from the previous more-significant word. * @@ -308,5 +316,4 @@ private static long shiftRightWord(final long value, final long prevValue, final } // endregion - } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java new file mode 100644 index 00000000000..5f5655130e0 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java @@ -0,0 +1,592 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.Code; +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.SarOperation; +import org.hyperledger.besu.evm.operation.ShlOperation; +import org.hyperledger.besu.evm.operation.ShrOperation; +import org.hyperledger.besu.evm.v2.StackArithmetic; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.ArrayDeque; +import java.util.Deque; + +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * Property-based tests comparing v2 StackArithmetic shift operations against the original + * non-optimized Bytes-based implementations. + */ +public class ShiftOperationsV2PropertyBasedTest { + + // region Arbitrary Providers + + @Provide + Arbitrary values1to32() { + return Arbitraries.bytes().array(byte[].class).ofMinSize(1).ofMaxSize(32); + } + + @Provide + Arbitrary shiftAmounts() { + return Arbitraries.bytes().array(byte[].class).ofMinSize(0).ofMaxSize(32); + } + + @Provide + Arbitrary smallShifts() { + return Arbitraries.integers().between(0, 255); + } + + @Provide + Arbitrary overflowShifts() { + return Arbitraries.integers().between(256, 1024); + } + + @Provide + Arbitrary negativeValues() { + return Arbitraries.bytes() + .array(byte[].class) + .ofSize(32) + .map( + bytes -> { + bytes[0] = (byte) (bytes[0] | 0x80); + return bytes; + }); + } + + @Provide + Arbitrary positiveValues() { + return Arbitraries.bytes() + .array(byte[].class) + .ofMinSize(1) + .ofMaxSize(32) + .map( + bytes -> { + bytes[0] = (byte) (bytes[0] & 0x7F); + return bytes; + }); + } + + // endregion + + // region SHL Property Tests + + @Property(tries = 10000) + void property_shlV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shift, value)); + final Bytes32 v2Result = runV2Shl(shift, value); + + assertThat(v2Result) + .as( + "SHL v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_shlV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shiftBytes, value)); + final Bytes32 v2Result = runV2Shl(shiftBytes, value); + + assertThat(v2Result).as("SHL v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_shlV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShl(shiftBytes, value)); + final Bytes32 v2Result = runV2Shl(shiftBytes, value); + + assertThat(v2Result) + .as("SHL v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SHR Property Tests + + @Property(tries = 10000) + void property_shrV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shift, value)); + final Bytes32 v2Result = runV2Shr(shift, value); + + assertThat(v2Result) + .as( + "SHR v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_shrV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shiftBytes, value)); + final Bytes32 v2Result = runV2Shr(shiftBytes, value); + + assertThat(v2Result).as("SHR v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_shrV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalShr(shiftBytes, value)); + final Bytes32 v2Result = runV2Shr(shiftBytes, value); + + assertThat(v2Result) + .as("SHR v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Random Inputs + + @Property(tries = 10000) + void property_sarV2_matchesOriginal_randomInputs( + @ForAll("values1to32") final byte[] valueBytes, + @ForAll("shiftAmounts") final byte[] shiftBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.wrap(shiftBytes); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as( + "SAR v2 mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_smallShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result).as("SAR v2 mismatch for shift=%d", shift).isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Negative Values (Sign Extension) + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_negativeValues_smallShifts( + @ForAll("negativeValues") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 negative mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_negativeValues_overflowShifts( + @ForAll("negativeValues") final byte[] valueBytes, + @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 negative overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Negative/Positive Values at Shift 255 + + @Property(tries = 3000) + void property_sarV2_matchesOriginal_negativeValues_shift255( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as("SAR v2 negative shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 3000) + void property_sarV2_matchesOriginal_positiveValues_shift255( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shift, value)); + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result) + .as("SAR v2 positive shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(originalResult); + } + + // endregion + + // region SAR Property Tests - Positive Values + + @Property(tries = 5000) + void property_sarV2_matchesOriginal_positiveValues_smallShifts( + @ForAll("positiveValues") final byte[] valueBytes, @ForAll("smallShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 positive mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(originalResult); + } + + @Property(tries = 1000) + void property_sarV2_matchesOriginal_positiveValues_overflowShifts( + @ForAll("positiveValues") final byte[] valueBytes, + @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes32 originalResult = Bytes32.leftPad(runOriginalSar(shiftBytes, value)); + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result) + .as("SAR v2 positive overflow mismatch for shift=%d", shift) + .isEqualTo(originalResult); + } + + // endregion + + // region Edge Case Tests - SHL / SHR + + @Property(tries = 1000) + void property_shlV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Shl(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 1000) + void property_shrV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Shr(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_shlV2_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Shl(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_shrV2_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Shr(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + // endregion + + // region Edge Case Tests - SAR + + @Property(tries = 1000) + void property_sarV2_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sarV2_negativeValue_largeShift_returnsAllOnes( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Sar(largeShift, value); + + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(v2Result).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sarV2_positiveValue_largeShift_returnsZero( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes32 v2Result = runV2Sar(largeShift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_sarV2_allOnes_anyShift_returnsAllOnes(@ForAll("smallShifts") final int shift) { + + final Bytes value = + Bytes.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes32 v2Result = runV2Sar(shiftBytes, value); + + assertThat(v2Result).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sarV2_minValue_shift255_returnsAllOnes() { + + final Bytes value = + Bytes.fromHexString("0x8000000000000000000000000000000000000000000000000000000000000000"); + final Bytes shift = Bytes.of(255); + + final Bytes32 v2Result = runV2Sar(shift, value); + + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(v2Result).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sarV2_maxPositive_shift255_returnsZero() { + + final Bytes value = + Bytes.fromHexString("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shift = Bytes.of(255); + + final Bytes32 v2Result = runV2Sar(shift, value); + + assertThat(v2Result).isEqualTo(Bytes32.ZERO); + } + + // endregion + + // region V1 Oracle Helpers (mock-based, Bytes stack) + + private Bytes runOriginalShl(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, ShlOperation::staticOperation); + } + + private Bytes runOriginalShr(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, ShrOperation::staticOperation); + } + + private Bytes runOriginalSar(final Bytes shift, final Bytes value) { + return runOriginalOperation(shift, value, SarOperation::staticOperation); + } + + @FunctionalInterface + interface OriginalOperationExecutor { + Operation.OperationResult execute(MessageFrame frame); + } + + private Bytes runOriginalOperation( + final Bytes shift, final Bytes value, final OriginalOperationExecutor executor) { + final MessageFrame frame = mock(MessageFrame.class); + final Deque stack = new ArrayDeque<>(); + stack.push(value); + stack.push(shift); + + when(frame.popStackItem()).thenAnswer(invocation -> stack.pop()); + + final Bytes[] result = new Bytes[1]; + doAnswer( + invocation -> { + result[0] = invocation.getArgument(0); + return null; + }) + .when(frame) + .pushStackItem(any(Bytes.class)); + + executor.execute(frame); + return result[0]; + } + + // endregion + + // region V2 Helpers (long[] stack) + + private Bytes32 runV2Shl(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::shl); + } + + private Bytes32 runV2Shr(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::shr); + } + + private Bytes32 runV2Sar(final Bytes shift, final Bytes value) { + return runV2Operation(shift, value, StackArithmetic::sar); + } + + @FunctionalInterface + interface V2OperationExecutor { + int execute(long[] stack, int top); + } + + private Bytes32 runV2Operation( + final Bytes shift, final Bytes value, final V2OperationExecutor executor) { + final UInt256 shiftVal = UInt256.fromBytesBE(Bytes32.leftPad(shift).toArrayUnsafe()); + final UInt256 valueVal = UInt256.fromBytesBE(Bytes32.leftPad(value).toArrayUnsafe()); + + final long[] s = new long[8]; + writeLimbs(s, 0, valueVal); + writeLimbs(s, 4, shiftVal); + + executor.execute(s, 2); + + final UInt256 result = new UInt256(s[0], s[1], s[2], s[3]); + return Bytes32.wrap(result.toBytesBE()); + } + + private static void writeLimbs(final long[] s, final int offset, final UInt256 val) { + s[offset] = val.u3(); + s[offset + 1] = val.u2(); + s[offset + 2] = val.u1(); + s[offset + 3] = val.u0(); + } + + // endregion + + // region Utility + + private Bytes intToMinimalBytes(final int value) { + if (value == 0) { + return Bytes.EMPTY; + } + if (value <= 0xFF) { + return Bytes.of(value); + } + if (value <= 0xFFFF) { + return Bytes.of(value >> 8, value & 0xFF); + } + if (value <= 0xFFFFFF) { + return Bytes.of(value >> 16, (value >> 8) & 0xFF, value & 0xFF); + } + return Bytes.of(value >> 24, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + } + + // endregion +} From 9ccfea9f8039af1bfa6db357af026ceb1534df7c Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 18:39:07 +0200 Subject: [PATCH 7/9] spotless Signed-off-by: Ameziane H. --- .../v2/operation/ShiftOperationsV2PropertyBasedTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java index 5f5655130e0..e9d43c145ed 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java @@ -20,19 +20,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.evm.Code; import org.hyperledger.besu.evm.UInt256; -import org.hyperledger.besu.evm.frame.BlockValues; import org.hyperledger.besu.evm.frame.MessageFrame; import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.operation.SarOperation; import org.hyperledger.besu.evm.operation.ShlOperation; import org.hyperledger.besu.evm.operation.ShrOperation; import org.hyperledger.besu.evm.v2.StackArithmetic; -import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.ArrayDeque; import java.util.Deque; From 0b688ffd320d28a1546fac5e37414e50642e85be Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Thu, 2 Apr 2026 20:08:14 +0200 Subject: [PATCH 8/9] Fix javadoc Signed-off-by: Ameziane H. --- .../hyperledger/besu/evm/v2/operation/SarOperationV2.java | 1 + .../hyperledger/besu/evm/v2/operation/ShlOperationV2.java | 5 +++-- .../hyperledger/besu/evm/v2/operation/ShrOperationV2.java | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java index 4a3dafc1b1f..d488886c5df 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java @@ -45,6 +45,7 @@ public Operation.OperationResult executeFixedCostOperation( * Performs sar operation. * * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) * @return the operation result */ public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java index 8aa84cab477..2f765e94269 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java @@ -45,11 +45,12 @@ public Operation.OperationResult executeFixedCostOperation( * Performs Shift Left operation. * * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) * @return the operation result */ - public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; - frame.setTopV2(StackArithmetic.shl(s, frame.stackTopV2())); + frame.setTopV2(StackArithmetic.shl(stack, frame.stackTopV2())); return shlSuccess; } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java index feebb57e702..71c412a04ab 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java @@ -45,11 +45,12 @@ public Operation.OperationResult executeFixedCostOperation( * Performs SHR operation. * * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) * @return the operation result */ - public static OperationResult staticOperation(final MessageFrame frame, final long[] s) { + public static OperationResult staticOperation(final MessageFrame frame, final long[] stack) { if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; - frame.setTopV2(StackArithmetic.shr(s, frame.stackTopV2())); + frame.setTopV2(StackArithmetic.shr(stack, frame.stackTopV2())); return shrSuccess; } } From c4c7c3dd8e35ce4f7338b9cb1e63fe5e5dd8bc7d Mon Sep 17 00:00:00 2001 From: "Ameziane H." Date: Tue, 7 Apr 2026 12:17:52 +0200 Subject: [PATCH 9/9] address comments. Signed-off-by: Ameziane H. --- .../v2/AbstractShiftOperationBenchmarkV2.java | 2 +- .../besu/evm/v2/StackArithmetic.java | 47 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java index cd83e28f46f..31e64b2962f 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/AbstractShiftOperationBenchmarkV2.java @@ -38,7 +38,7 @@ public enum Case { SHIFT_0, /** Small shift by 1 bit. */ SHIFT_1, - /** Medium shift by 128 bits (half word). */ + /** Medium shift by 128 bits. */ SHIFT_128, /** Large shift by 255 bits (max valid). */ SHIFT_255, diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java index a9685d4ffd6..6a9f808f3dc 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -33,8 +33,9 @@ private StackArithmetic() {} /** * Performs EVM SHL (shift left) on the two top stack items. * - *

Pops the shift amount (unsigned) and the value, pushes {@code value << shift}. Shifts >= 256 - * or a zero value produce 0. + *

Reads the shift amount (unsigned) and the value from the top two stack slots, writes {@code + * value << shift} back into the value slot and decrements the top. Shifts >= 256 or a zero value + * produce 0. * * @param stack the flat limb array * @param top current stack-top (item count) @@ -76,7 +77,9 @@ private static void shiftLeftInPlace(final long[] stack, final int valueOffset, w1 = stack[valueOffset + 1], w2 = stack[valueOffset + 2], w3 = stack[valueOffset + 3]; + // Number of whole 64-bit words to shift (shift / 64) final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64) final int bitShift = shift & 63; switch (wordShift) { case 0: @@ -132,8 +135,9 @@ private static long shiftLeftWord(final long value, final long nextValue, final /** * Performs EVM SHR (logical shift right) on the two top stack items. * - *

Pops the shift amount (unsigned) and the value, pushes {@code value >>> shift}. Shifts >= - * 256 or a zero value produce 0. + *

Reads the shift amount (unsigned) and the value from the top two stack slots, writes {@code + * value >>> shift} back into the value slot and decrements the top. Shifts >= 256 or a zero value + * produce 0. * * @param stack the flat limb array * @param top current stack-top (item count) @@ -164,17 +168,20 @@ public static int shr(final long[] stack, final int top) { /** * Logically right-shifts a 256-bit value in place by 1..255 bits, zero-filling from the left. * - * @param s the flat limb array + * @param stack the flat limb array * @param valueOffset index of the value's most-significant limb * @param shift number of bits to shift (must be in [1, 255]) */ - private static void shiftRightInPlace(final long[] s, final int valueOffset, final int shift) { + private static void shiftRightInPlace( + final long[] stack, final int valueOffset, final int shift) { if (shift == 0) return; - long w0 = s[valueOffset], - w1 = s[valueOffset + 1], - w2 = s[valueOffset + 2], - w3 = s[valueOffset + 3]; + long w0 = stack[valueOffset], + w1 = stack[valueOffset + 1], + w2 = stack[valueOffset + 2], + w3 = stack[valueOffset + 3]; + // Number of whole 64-bit words to shift (shift / 64) final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64) final int bitShift = shift & 63; switch (wordShift) { case 0: @@ -202,10 +209,10 @@ private static void shiftRightInPlace(final long[] s, final int valueOffset, fin w0 = 0; break; } - s[valueOffset] = w0; - s[valueOffset + 1] = w1; - s[valueOffset + 2] = w2; - s[valueOffset + 3] = w3; + stack[valueOffset] = w0; + stack[valueOffset + 1] = w1; + stack[valueOffset + 2] = w2; + stack[valueOffset + 3] = w3; } // endregion @@ -216,8 +223,9 @@ private static void shiftRightInPlace(final long[] s, final int valueOffset, fin /** * Performs EVM SAR (arithmetic shift right) on the two top stack items. * - *

Pops the shift amount (unsigned) and the value (signed), pushes {@code value >> shift}. - * Shifts >= 256 produce 0 for positive values and -1 for negative values. + *

Reads the shift amount (unsigned) and the value (signed) from the top two stack slots, + * writes {@code value >> shift} back into the value slot and decrements the top. Shifts >= 256 + * produce 0 for positive values and -1 for negative values. * * @param stack the flat limb array * @param top current stack-top (item count) @@ -260,7 +268,9 @@ private static void sarInPlace( w2 = stack[valueOffset + 2], w3 = stack[valueOffset + 3]; final long fill = negative ? -1L : 0L; + // Number of whole 64-bit words to shift (shift / 64) final int wordShift = shift >>> 6; + // Remaining intra-word bit shift (shift % 64) final int bitShift = shift & 63; switch (wordShift) { case 0: @@ -294,11 +304,6 @@ private static void sarInPlace( stack[valueOffset + 3] = w3; } - // endregion - - // region Private Helpers - // --------------------------------------------------------------------------- - /** * Shifts a 64-bit word right and carries in bits from the previous more-significant word. *