diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f3542e8cf..7ceb8a817de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ #### Performance - EVM optimisations - Improves 70% of EEST benchmarks [#9775](https://github.com/hyperledger/besu/pull/9775) +- EVM optimisations - Improve SAR, SHR and SHL opcodes performance [#9796](https://github.com/hyperledger/besu/pull/9796) ### Bug fixes - Fix QBFT Shanghai support by reintroducing NotApplicableWithdrawals withdrawals validator [#9830](https://github.com/hyperledger/besu/pull/9830) diff --git a/build.gradle b/build.gradle index 909ee98f7e9..e91a23e24d3 100644 --- a/build.gradle +++ b/build.gradle @@ -656,6 +656,7 @@ subprojects { excludes = _strListCmdArg('excludes', []) var asyncProfiler = _strCmdArg('asyncProfiler') var asyncProfilerOptions = _strCmdArg('asyncProfilerOptions', 'output=flamegraph') + zip64.set(true) jvmArgs = [ '-XX:+EnableDynamicAgentLoading' ] @@ -666,6 +667,10 @@ subprojects { // Force debug symbols of stack traces to the profiler jvmArgs.addAll("-XX:+UnlockDiagnosticVMOptions", "-XX:+DebugNonSafepoints") } + var gcProfiler = _strCmdArg('gcProfiler') + if (gcProfiler != null && gcProfiler.toBoolean()) { + profilers.add('gc') + } duplicateClassesStrategy = DuplicatesStrategy.INCLUDE } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractSarOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractSarOperationBenchmark.java new file mode 100644 index 00000000000..786fd254408 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractSarOperationBenchmark.java @@ -0,0 +1,161 @@ +/* + * 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; + +import static org.hyperledger.besu.ethereum.vm.operations.BenchmarkHelper.randomNegativeValue; +import static org.hyperledger.besu.ethereum.vm.operations.BenchmarkHelper.randomPositiveValue; +import static org.hyperledger.besu.ethereum.vm.operations.BenchmarkHelper.randomValue; + +import java.util.concurrent.ThreadLocalRandom; + +import org.apache.tuweni.bytes.Bytes; +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 AbstractSarOperationBenchmark extends BinaryOperationBenchmark { + + /** 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 + } + + /** All bits set (32 bytes of 0xFF) - represents -1 in two's complement. */ + protected static final Bytes ALL_BITS = + Bytes.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + @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 = BenchmarkHelper.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new Bytes[SAMPLE_SIZE]; // shift amount (pushed second, popped first) + bPool = new Bytes[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] = Bytes.of(0); + bPool[i] = randomValue(random); + break; + + case NEGATIVE_SHIFT_1: + // shiftAmount = 0x1, value = 0xfff...fff (negative, tests OR path) + aPool[i] = Bytes.of(1); + bPool[i] = randomNegativeValue(random); + break; + + case ALL_BITS_SHIFT_1: + // shiftAmount = 0x1, value = 0xfff...fff (negative, tests OR path) + aPool[i] = Bytes.of(1); + bPool[i] = ALL_BITS; + break; + + case POSITIVE_SHIFT_1: + // shiftAmount = 0x1, random positive value (no sign extension) + aPool[i] = Bytes.of(1); + bPool[i] = randomPositiveValue(random); + break; + + case NEGATIVE_SHIFT_128: + aPool[i] = Bytes.of(128); + bPool[i] = randomNegativeValue(random); + break; + + case NEGATIVE_SHIFT_255: + aPool[i] = Bytes.of(255); + bPool[i] = randomNegativeValue(random); + break; + + case POSITIVE_SHIFT_128: + aPool[i] = Bytes.of(128); + bPool[i] = randomPositiveValue(random); + break; + case POSITIVE_SHIFT_255: + aPool[i] = Bytes.of(255); + bPool[i] = randomPositiveValue(random); + break; + + case OVERFLOW_SHIFT_256: + // Shift of exactly 256 - overflow path + aPool[i] = Bytes.fromHexString("0x0100"); // 256 + bPool[i] = randomValue(random); + break; + + case OVERFLOW_LARGE_SHIFT: + // Shift amount > 4 bytes - overflow path + aPool[i] = Bytes.fromHexString("0x010000000000"); // > 4 bytes + bPool[i] = randomValue(random); + break; + + case FULL_RANDOM: + default: + // Original random behavior + 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] = Bytes.wrap(shift); + bPool[i] = Bytes.wrap(value); + break; + } + } + index = 0; + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractShiftOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractShiftOperationBenchmark.java new file mode 100644 index 00000000000..7ccd39cfec2 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/AbstractShiftOperationBenchmark.java @@ -0,0 +1,118 @@ +/* + * 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; + +import static org.hyperledger.besu.ethereum.vm.operations.BenchmarkHelper.randomValue; + +import java.util.concurrent.ThreadLocalRandom; + +import org.apache.tuweni.bytes.Bytes; +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 AbstractShiftOperationBenchmark extends BinaryOperationBenchmark { + + /** 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 = BenchmarkHelper.createMessageCallFrame(); + + final Case scenario = Case.valueOf(caseName); + aPool = new Bytes[SAMPLE_SIZE]; // shift amount (pushed second, popped first) + bPool = new Bytes[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] = Bytes.of(0); + bPool[i] = randomValue(random); + break; + + case SHIFT_1: + aPool[i] = Bytes.of(1); + bPool[i] = randomValue(random); + break; + + case SHIFT_128: + aPool[i] = Bytes.of(128); + bPool[i] = randomValue(random); + break; + + case SHIFT_255: + aPool[i] = Bytes.of(255); + bPool[i] = randomValue(random); + break; + + case OVERFLOW_SHIFT_256: + aPool[i] = Bytes.fromHexString("0x0100"); // 256 + bPool[i] = randomValue(random); + break; + + case OVERFLOW_LARGE_SHIFT: + aPool[i] = Bytes.fromHexString("0x010000000000"); // > 4 bytes + bPool[i] = randomValue(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] = Bytes.wrap(shift); + bPool[i] = Bytes.wrap(value); + break; + } + } + index = 0; + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BenchmarkHelper.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BenchmarkHelper.java index fffa86ee124..cda01bfb05f 100644 --- a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BenchmarkHelper.java +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/BenchmarkHelper.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import java.util.function.BiPredicate; import java.util.function.Predicate; import java.util.function.Supplier; @@ -34,6 +35,13 @@ import org.apache.tuweni.units.bigints.UInt256; public class BenchmarkHelper { + /** + * Creates a minimal {@link MessageFrame} suitable for opcode benchmarks. + * + *

The frame is configured with mocked dependencies and deterministic zero/default values. + * + * @return a message-call frame ready to use in benchmark setup + */ public static MessageFrame createMessageCallFrame() { return MessageFrame.builder() .worldUpdater(mock(WorldUpdater.class)) @@ -56,6 +64,12 @@ public static MessageFrame createMessageCallFrame() { .build(); } + /** + * Creates a minimal {@link MessageFrame} with custom call data for benchmarks. + * + * @param callData the input data to attach to the frame + * @return a message-call frame initialized with {@code callData} + */ public static MessageFrame createMessageCallFrameWithCallData(final Bytes callData) { return MessageFrame.builder() .worldUpdater(mock(WorldUpdater.class)) @@ -164,6 +178,13 @@ public static void fillPools( } } + /** + * Creates call data payload for benchmarks. + * + * @param size size of the payload in bytes + * @param nonZero whether to fill payload with deterministic non-zero bytes + * @return call data payload + */ static Bytes createCallData(final int size, final boolean nonZero) { byte[] data = new byte[size]; if (nonZero) { @@ -174,6 +195,15 @@ static Bytes createCallData(final int size, final boolean nonZero) { return Bytes.wrap(data); } + /** + * Fills COPY-like benchmark pools for call-data operations. + * + * @param sizePool destination pool for copy sizes + * @param destOffsetPool destination pool for destination offsets + * @param srcOffsetPool destination pool for source offsets + * @param dataSize call-data size used to populate the size/source ranges + * @param fixedSrcDst whether to use fixed zero source/destination offsets + */ static void fillPoolsForCallData( final Bytes[] sizePool, final Bytes[] destOffsetPool, @@ -192,4 +222,42 @@ static void fillPoolsForCallData( } } } + + /** + * Generates a random 32-byte value. + * + * @param random thread-local random source + * @return random 32-byte value + */ + static Bytes randomValue(final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + return Bytes.wrap(value); + } + + /** + * Generates a random positive signed 256-bit value (sign bit cleared). + * + * @param random thread-local random source + * @return random positive 32-byte value + */ + static Bytes randomPositiveValue(final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] & 0x7F); + return Bytes.wrap(value); + } + + /** + * Generates a random negative signed 256-bit value (sign bit set). + * + * @param random thread-local random source + * @return random negative 32-byte value + */ + static Bytes randomNegativeValue(final ThreadLocalRandom random) { + final byte[] value = new byte[32]; + random.nextBytes(value); + value[0] = (byte) (value[0] | 0x80); + return Bytes.wrap(value); + } } diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationBenchmark.java new file mode 100644 index 00000000000..83e6db2ed45 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.SarOperation; + +/** JMH benchmark for the original SAR (Shift Arithmetic Right) operation. */ +public class SarOperationBenchmark extends AbstractSarOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return SarOperation.staticOperation(frame); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationOptimizedBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationOptimizedBenchmark.java new file mode 100644 index 00000000000..ceea8d402f4 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/SarOperationOptimizedBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.SarOperationOptimized; + +/** JMH benchmark for the optimized SAR (Shift Arithmetic Right) operation. */ +public class SarOperationOptimizedBenchmark extends AbstractSarOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return SarOperationOptimized.staticOperation(frame); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationBenchmark.java new file mode 100644 index 00000000000..791cc6a95dd --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.ShlOperation; + +/** JMH benchmark for the original SHL (Shift Left) operation. */ +public class ShlOperationBenchmark extends AbstractShiftOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShlOperation.staticOperation(frame); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationOptimizedBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationOptimizedBenchmark.java new file mode 100644 index 00000000000..aa88abe98de --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShlOperationOptimizedBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.ShlOperationOptimized; + +/** JMH benchmark for the optimized SHL (Shift Left) operation. */ +public class ShlOperationOptimizedBenchmark extends AbstractShiftOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShlOperationOptimized.staticOperation(frame); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationBenchmark.java new file mode 100644 index 00000000000..f4bcb2c28d7 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.ShrOperation; + +/** JMH benchmark for the original SHR (Shift Right Logical) operation. */ +public class ShrOperationBenchmark extends AbstractShiftOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShrOperation.staticOperation(frame); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationOptimizedBenchmark.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationOptimizedBenchmark.java new file mode 100644 index 00000000000..2cf2a31bba3 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/ShrOperationOptimizedBenchmark.java @@ -0,0 +1,28 @@ +/* + * 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; + +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.operation.ShrOperationOptimized; + +/** JMH benchmark for the optimized SHR (Shift Right Logical) operation. */ +public class ShrOperationOptimizedBenchmark extends AbstractShiftOperationBenchmark { + + @Override + protected Operation.OperationResult invoke(final MessageFrame frame) { + return ShrOperationOptimized.staticOperation(frame); + } +} 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 6b173b78ead..95d3dcc7285 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -68,6 +68,12 @@ import org.hyperledger.besu.evm.operation.SLtOperation; import org.hyperledger.besu.evm.operation.SModOperation; import org.hyperledger.besu.evm.operation.SModOperationOptimized; +import org.hyperledger.besu.evm.operation.SarOperation; +import org.hyperledger.besu.evm.operation.SarOperationOptimized; +import org.hyperledger.besu.evm.operation.ShlOperation; +import org.hyperledger.besu.evm.operation.ShlOperationOptimized; +import org.hyperledger.besu.evm.operation.ShrOperation; +import org.hyperledger.besu.evm.operation.ShrOperationOptimized; import org.hyperledger.besu.evm.operation.SignExtendOperation; import org.hyperledger.besu.evm.operation.StopOperation; import org.hyperledger.besu.evm.operation.SubOperation; @@ -280,6 +286,18 @@ public void runToHalt(final MessageFrame frame, final OperationTracer tracing) { ? NotOperationOptimized.staticOperation(frame) : NotOperation.staticOperation(frame); case 0x1a -> ByteOperation.staticOperation(frame); + case 0x1b -> + evmConfiguration.enableOptimizedOpcodes() + ? ShlOperationOptimized.staticOperation(frame) + : ShlOperation.staticOperation(frame); + case 0x1c -> + evmConfiguration.enableOptimizedOpcodes() + ? ShrOperationOptimized.staticOperation(frame) + : ShrOperation.staticOperation(frame); + case 0x1d -> + evmConfiguration.enableOptimizedOpcodes() + ? SarOperationOptimized.staticOperation(frame) + : SarOperation.staticOperation(frame); case 0x1e -> enableOsaka ? CountLeadingZerosOperation.staticOperation(frame) diff --git a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java index 9a584fde496..120893515f7 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java @@ -116,10 +116,13 @@ import org.hyperledger.besu.evm.operation.SModOperationOptimized; import org.hyperledger.besu.evm.operation.SStoreOperation; import org.hyperledger.besu.evm.operation.SarOperation; +import org.hyperledger.besu.evm.operation.SarOperationOptimized; import org.hyperledger.besu.evm.operation.SelfBalanceOperation; import org.hyperledger.besu.evm.operation.SelfDestructOperation; import org.hyperledger.besu.evm.operation.ShlOperation; +import org.hyperledger.besu.evm.operation.ShlOperationOptimized; import org.hyperledger.besu.evm.operation.ShrOperation; +import org.hyperledger.besu.evm.operation.ShrOperationOptimized; import org.hyperledger.besu.evm.operation.SignExtendOperation; import org.hyperledger.besu.evm.operation.SlotNumOperation; import org.hyperledger.besu.evm.operation.StaticCallOperation; @@ -493,9 +496,15 @@ private static void registerConstantinopleOperations( final EvmConfiguration evmConfiguration) { registerByzantiumOperations(registry, gasCalculator, evmConfiguration); registry.put(new Create2Operation(gasCalculator)); - registry.put(new SarOperation(gasCalculator)); - registry.put(new ShlOperation(gasCalculator)); - registry.put(new ShrOperation(gasCalculator)); + if (evmConfiguration.enableOptimizedOpcodes()) { + registry.put(new ShlOperationOptimized(gasCalculator)); + registry.put(new ShrOperationOptimized(gasCalculator)); + registry.put(new SarOperationOptimized(gasCalculator)); + } else { + registry.put(new ShlOperation(gasCalculator)); + registry.put(new ShrOperation(gasCalculator)); + registry.put(new SarOperation(gasCalculator)); + } registry.put(new ExtCodeHashOperation(gasCalculator)); } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java new file mode 100644 index 00000000000..adbea0cb47a --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/SarOperationOptimized.java @@ -0,0 +1,123 @@ +/* + * 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.operation; + +import static org.hyperledger.besu.evm.operation.Shift256Operations.ALL_ONES; +import static org.hyperledger.besu.evm.operation.Shift256Operations.ALL_ONES_BYTES; +import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** The Sar operation. */ +public class SarOperationOptimized extends AbstractFixedCostOperation { + + /** The Sar operation success result. */ + static final OperationResult sarSuccess = new OperationResult(3, null); + + /** + * Instantiates a new Sar operation. + * + * @param gasCalculator the gas calculator + */ + public SarOperationOptimized(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); + } + + /** + * Performs sar operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + final Bytes shiftAmount = frame.popStackItem(); + final Bytes value = frame.popStackItem(); + byte[] valueBytes = value.toArrayUnsafe(); + if (Arrays.equals(valueBytes, ALL_ONES_BYTES)) { + frame.pushStackItem(ALL_ONES); + return sarSuccess; + } + valueBytes = Bytes32.leftPad(value).toArrayUnsafe(); + final byte[] shiftBytes = shiftAmount.toArrayUnsafe(); + final boolean negative = (valueBytes[0] & 0x80) != 0; + + // shift >= 256, push All 1s if negative, All 0s otherwise + if (isShiftOverflow(shiftBytes)) { + frame.pushStackItem(negative ? ALL_ONES : Bytes.EMPTY); + return sarSuccess; + } + final int shift = shiftBytes.length == 0 ? 0 : (shiftBytes[shiftBytes.length - 1] & 0xFF); + + frame.pushStackItem(sar256(valueBytes, shift, negative)); + return sarSuccess; + } + + /** + * Performs a 256-bit arithmetic right shift (EVM SAR). + * + *

The input value is treated as a signed 256-bit integer in two's complement representation. + * The shift amount is in the range {@code [0..255]} and is assumed to have been validated by the + * caller. + * + *

For shift values greater than or equal to 256, the result is fully sign-extended and handled + * by the caller. + * + * @param in the raw 32-byte array of the input value + * @param shift the right shift amount in bits (0–255) + * @param negative whether the input value is negative (sign bit set) + * @return the shifted 256-bit value + */ + private static Bytes sar256(final byte[] in, final int shift, final boolean negative) { + if (shift == 0) return Bytes.wrap(in); + + final int shiftBytes = shift >>> 3; // /8 + final int shiftBits = shift & 7; // %8 + final int fill = negative ? 0xFF : 0x00; + + final byte[] out = new byte[32]; + + // Pre-fill sign-extended bytes (indices below shiftBytes are fully sign-extended) + if (negative && shiftBytes > 0) { + Arrays.fill(out, 0, shiftBytes, (byte) 0xFF); + } + + // Only iterate bytes that receive shifted data from the input + for (int i = 31; i >= shiftBytes; i--) { + final int srcIndex = i - shiftBytes; + final int curr = in[srcIndex] & 0xFF; + if (shiftBits == 0) { + out[i] = (byte) curr; + } else { + final int prev = (srcIndex - 1 >= 0) ? (in[srcIndex - 1] & 0xFF) : fill; + out[i] = (byte) ((curr >>> shiftBits) | (prev << (8 - shiftBits))); + } + } + + return Bytes.wrap(out); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java new file mode 100644 index 00000000000..edfdab6be03 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/Shift256Operations.java @@ -0,0 +1,53 @@ +/* + * 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.operation; + +import java.util.Arrays; + +import org.apache.tuweni.bytes.Bytes; + +/** + * Utility class for shared constants and helpers used by optimized 256-bit shift operations (SHL, + * SHR, SAR). + */ +public final class Shift256Operations { + + /** An array of 31 0 bytes */ + private static final byte[] ZERO_31 = new byte[31]; + + /** All ones (0xFF repeated 32 times). */ + public static final Bytes ALL_ONES = Bytes.repeat((byte) 0xFF, 32); + + /** Raw byte array of ALL_ONES for use with {@code Arrays.equals} (JVM intrinsic). */ + static final byte[] ALL_ONES_BYTES = ALL_ONES.toArrayUnsafe(); + + private Shift256Operations() { + // Utility class - prevent instantiation + } + + /** + * Checks whether the EVM shift amount overflows (shift >= 256). + * + *

If any byte except the last byte is non-zero, the value is >= 256. + * + * @param shiftBytes the raw byte array of the shift amount + * @return {@code true} if the shift amount is >= 256, {@code false} otherwise + */ + public static boolean isShiftOverflow(final byte[] shiftBytes) { + final int len = shiftBytes.length - 1; + if (len <= 0) return false; + return !Arrays.equals(shiftBytes, 0, len, ZERO_31, 0, len); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java new file mode 100644 index 00000000000..a691636e060 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShlOperationOptimized.java @@ -0,0 +1,117 @@ +/* + * 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.operation; + +import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * The optimized SHL (Shift Left) operation. + * + *

This implementation uses direct byte[] manipulation instead of Tuweni's Bytes.shiftLeft() to + * avoid intermediate object allocation and improve performance. + */ +public class ShlOperationOptimized extends AbstractFixedCostOperation { + + /** The Shl operation success result. */ + static final OperationResult shlSuccess = new OperationResult(3, null); + + /** + * Instantiates a new optimized Shl operation. + * + * @param gasCalculator the gas calculator + */ + public ShlOperationOptimized(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); + } + + /** + * Performs optimized Shift Left operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + final Bytes shiftAmount = frame.popStackItem(); + final Bytes value = frame.popStackItem(); + if (value.isZero()) { + frame.pushStackItem(Bytes.EMPTY); + return shlSuccess; + } + + final byte[] valueBytes = Bytes32.leftPad(value).toArrayUnsafe(); + final byte[] shiftBytes = shiftAmount.toArrayUnsafe(); + + // shift >= 256, push All 0s + if (isShiftOverflow(shiftBytes)) { + frame.pushStackItem(Bytes.EMPTY); + return shlSuccess; + } + + final int shift = shiftBytes.length == 0 ? 0 : (shiftBytes[shiftBytes.length - 1] & 0xFF); + + frame.pushStackItem(shl256(valueBytes, shift)); + return shlSuccess; + } + + /** + * Performs a 256-bit logical left shift (EVM SHL). + * + *

The shift amount is in the range {@code [0..255]} and is assumed to have been validated by + * the caller. For shift values >= 256, zero is returned by the caller. + * + * @param in the raw 32-byte array of the input value + * @param shift the left shift amount in bits (0–255) + * @return the shifted 256-bit value + */ + private static Bytes shl256(final byte[] in, final int shift) { + if (shift == 0) { + return Bytes.wrap(in); + } + + final int shiftBytes = shift >>> 3; // /8 + final int shiftBits = shift & 7; // %8 + + final byte[] out = new byte[32]; + + // Shift left: bytes move to lower indices (towards index 0) + // Bytes at index >= (32 - shiftBytes) are guaranteed zero (already from new byte[32]) + final int limit = 32 - shiftBytes; + for (int i = 0; i < limit; i++) { + final int srcIndex = i + shiftBytes; + final int curr = in[srcIndex] & 0xFF; + if (shiftBits == 0) { + out[i] = (byte) curr; + } else { + final int next = (srcIndex + 1 < 32) ? (in[srcIndex + 1] & 0xFF) : 0; + out[i] = (byte) ((curr << shiftBits) | (next >>> (8 - shiftBits))); + } + } + + return Bytes.wrap(out); + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java new file mode 100644 index 00000000000..1eeb1b1ca5c --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/ShrOperationOptimized.java @@ -0,0 +1,116 @@ +/* + * 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.operation; + +import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; + +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * The optimized SHR (Shift Right Logical) operation. + * + *

This implementation uses direct byte[] manipulation instead of Tuweni's Bytes.shiftRight() to + * avoid intermediate object allocation and improve performance. + */ +public class ShrOperationOptimized extends AbstractFixedCostOperation { + + /** The Shr operation success result. */ + static final OperationResult shrSuccess = new OperationResult(3, null); + + /** + * Instantiates a new optimized Shr operation. + * + * @param gasCalculator the gas calculator + */ + public ShrOperationOptimized(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); + } + + /** + * Performs optimized Shift Right Logical operation. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + final Bytes shiftAmount = frame.popStackItem(); + final Bytes value = frame.popStackItem(); + if (value.isZero()) { + frame.pushStackItem(Bytes.EMPTY); + return shrSuccess; + } + + final byte[] valueBytes = Bytes32.leftPad(value).toArrayUnsafe(); + final byte[] shiftBytes = shiftAmount.toArrayUnsafe(); + + // shift >= 256, push All 0s + if (isShiftOverflow(shiftBytes)) { + frame.pushStackItem(Bytes.EMPTY); + return shrSuccess; + } + + final int shift = shiftBytes.length == 0 ? 0 : (shiftBytes[shiftBytes.length - 1] & 0xFF); + + frame.pushStackItem(shr256(valueBytes, shift)); + return shrSuccess; + } + + /** + * Performs a 256-bit logical right shift (EVM SHR). + * + *

The shift amount is in the range {@code [0..255]} and is assumed to have been validated by + * the caller. For shift values >= 256, zero is returned by the caller. + * + * @param in the raw 32-byte array of the input value + * @param shift the right shift amount in bits (0–255) + * @return the shifted 256-bit value + */ + private static Bytes shr256(final byte[] in, final int shift) { + if (shift == 0) { + return Bytes.wrap(in); + } + + final int shiftBytes = shift >>> 3; // /8 + final int shiftBits = shift & 7; // %8 + + final byte[] out = new byte[32]; + + // Shift right: bytes move to higher indices (towards index 31) + // Bytes below shiftBytes are guaranteed zero (already from new byte[32]) + for (int i = 31; i >= shiftBytes; i--) { + final int srcIndex = i - shiftBytes; + final int curr = in[srcIndex] & 0xFF; + if (shiftBits == 0) { + out[i] = (byte) curr; + } else { + final int prev = (srcIndex - 1 >= 0) ? (in[srcIndex - 1] & 0xFF) : 0; + out[i] = (byte) ((curr >>> shiftBits) | (prev << (8 - shiftBits))); + } + } + + return Bytes.wrap(out); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationPropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationPropertyBasedTest.java new file mode 100644 index 00000000000..8f89f50bbeb --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/SarOperationPropertyBasedTest.java @@ -0,0 +1,405 @@ +/* + * 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.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.evm.frame.MessageFrame; + +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 original SAR operation with the optimized version. + * + *

Tests verify that SarOperationOptimized produces identical results to SarOperation for all + * possible inputs, including edge cases for negative values and sign extension. + */ +public class SarOperationPropertyBasedTest { + + // 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); // set sign bit of 256-bit word + return bytes; + }); + } + + @Provide + Arbitrary positiveValues() { + // Generate values with sign bit clear (first byte < 0x80) + return Arbitraries.bytes() + .array(byte[].class) + .ofMinSize(1) + .ofMaxSize(32) + .map( + bytes -> { + if (bytes.length > 0) { + // Ensure sign bit is clear by ANDing with 0x7F + bytes[0] = (byte) (bytes[0] & 0x7F); + } + return bytes; + }); + } + + // endregion + + // region SAR Property Tests - Random Inputs + + @Property(tries = 10000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as( + "SAR mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 5000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 1000) + void property_sarOptimized_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR overflow mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region SAR Property Tests - Negative Values (Sign Extension) + + @Property(tries = 5000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR negative mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 1000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + // For negative values with overflow shift, result should be all ones + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR negative overflow mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region SAR Property Tests - Positive Values + + @Property(tries = 5000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR positive mismatch for shift=%d, value=%s", shift, value.toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 1000) + void property_sarOptimized_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 Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + // For positive values with overflow shift, result should be all zeros + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR positive overflow mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region Edge Case Tests + + @Property(tries = 1000) + void property_sar_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.leftPad(value)); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sar_negativeValue_largeShift_returnsAllOnes( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes originalResult = runSarOperation(largeShift, value); + final Bytes optimizedResult = runSarOperationOptimized(largeShift, value); + + // Both should return all ones for negative value with large shift + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(allOnes); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sar_positiveValue_largeShift_returnsZero( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes originalResult = runSarOperation(largeShift, value); + final Bytes optimizedResult = runSarOperationOptimized(largeShift, value); + + // Both should return zero for positive value with large shift + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.ZERO); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_sar_allOnes_anyShift_returnsAllOnes(@ForAll("smallShifts") final int shift) { + + // -1 in two's complement (all bits set) + final Bytes value = + Bytes.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shiftBytes = Bytes.of(shift); + + final Bytes originalResult = runSarOperation(shiftBytes, value); + final Bytes optimizedResult = runSarOperationOptimized(shiftBytes, value); + + // SAR of -1 by any amount should still be -1 (all ones) + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.leftPad(value)); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_sar_minValue_shift255_returnsAllOnes() { + + // MIN_VALUE: 0x8000...0000 (only sign bit set) + final Bytes value = + Bytes.fromHexString("0x8000000000000000000000000000000000000000000000000000000000000000"); + final Bytes shift = Bytes.of(255); + + final Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + // SAR of MIN_VALUE by 255 should be all ones (-1) + final Bytes32 allOnes = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(allOnes); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(allOnes); + } + + @Property(tries = 500) + void property_sar_maxPositive_shift255_returnsZero() { + + // MAX_VALUE: 0x7fff...ffff (all bits except sign bit set) + final Bytes value = + Bytes.fromHexString("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + final Bytes shift = Bytes.of(255); + + final Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + // SAR of MAX_VALUE by 255 should be 0 + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.ZERO); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 3000) + void property_sarOptimized_matchesOriginal_negativeValues_shift255( + @ForAll("negativeValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR negative shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 3000) + void property_sarOptimized_matchesOriginal_positiveValues_shift255( + @ForAll("positiveValues") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(255); + + final Bytes originalResult = runSarOperation(shift, value); + final Bytes optimizedResult = runSarOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SAR positive shift=255 mismatch for value=%s", value.toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region Helper Methods + + private Bytes runSarOperation(final Bytes shift, final Bytes value) { + return runOperation(shift, value, SarOperation::staticOperation); + } + + private Bytes runSarOperationOptimized(final Bytes shift, final Bytes value) { + return runOperation(shift, value, SarOperationOptimized::staticOperation); + } + + @FunctionalInterface + interface OperationExecutor { + Operation.OperationResult execute(MessageFrame frame); + } + + private Bytes runOperation( + final Bytes shift, final Bytes value, final OperationExecutor 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]; + } + + 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 +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/Shift256OperationsTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/Shift256OperationsTest.java new file mode 100644 index 00000000000..4e8ec768f6f --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/Shift256OperationsTest.java @@ -0,0 +1,80 @@ +/* + * 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.operation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.evm.operation.Shift256Operations.ALL_ONES; +import static org.hyperledger.besu.evm.operation.Shift256Operations.isShiftOverflow; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +/** Unit tests for the {@link Shift256Operations} utility class. */ +class Shift256OperationsTest { + + // region Constants Tests + + @Test + void constants_areCorrect() { + assertThat(ALL_ONES.size()).isEqualTo(32); + assertThat(ALL_ONES.toHexString()) + .isEqualTo("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + } + + // endregion + + // region isShiftOverflow Tests + + @Test + void isShiftOverflow_empty_returnsFalse() { + assertThat(isShiftOverflow(Bytes.EMPTY.toArrayUnsafe())).isFalse(); + } + + @Test + void isShiftOverflow_singleByte_returnsFalse() { + assertThat(isShiftOverflow(Bytes.of(0).toArrayUnsafe())).isFalse(); + assertThat(isShiftOverflow(Bytes.of(1).toArrayUnsafe())).isFalse(); + assertThat(isShiftOverflow(Bytes.of(128).toArrayUnsafe())).isFalse(); + assertThat(isShiftOverflow(Bytes.of(255).toArrayUnsafe())).isFalse(); + } + + @Test + void isShiftOverflow_multiByteWithZeroPrefix_returnsFalse() { + assertThat(isShiftOverflow(Bytes.of(0, 0, 42).toArrayUnsafe())).isFalse(); + assertThat(isShiftOverflow(Bytes.of(0, 0, 0, 0, 200).toArrayUnsafe())).isFalse(); + } + + @Test + void isShiftOverflow_256_returnsTrue() { + // 0x0100 = 256 + assertThat(isShiftOverflow(Bytes.fromHexString("0x0100").toArrayUnsafe())).isTrue(); + } + + @Test + void isShiftOverflow_nonZeroHighByte_returnsTrue() { + assertThat(isShiftOverflow(Bytes.of(1, 0).toArrayUnsafe())).isTrue(); + assertThat(isShiftOverflow(Bytes.of(0, 1, 0).toArrayUnsafe())).isTrue(); + assertThat(isShiftOverflow(Bytes.fromHexString("0x010000000000").toArrayUnsafe())).isTrue(); + } + + @Test + void isShiftOverflow_largeValue_returnsTrue() { + // Large shift amount should overflow + assertThat(isShiftOverflow(Bytes.fromHexString("0xff00").toArrayUnsafe())).isTrue(); + assertThat(isShiftOverflow(Bytes.fromHexString("0x0200").toArrayUnsafe())).isTrue(); // 512 + } + + // endregion +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operation/ShiftOperationsPropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShiftOperationsPropertyBasedTest.java new file mode 100644 index 00000000000..004acfd7ba6 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/operation/ShiftOperationsPropertyBasedTest.java @@ -0,0 +1,292 @@ +/* + * 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.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.evm.frame.MessageFrame; + +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 original shift operations with their optimized versions. + * + *

Tests verify that SHL/SHR optimized implementations produce identical results to the original + * implementations for all possible inputs. + */ +public class ShiftOperationsPropertyBasedTest { + + // 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); + } + + // endregion + + // region SHL Property Tests + + @Property(tries = 10000) + void property_shlOptimized_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 Bytes originalResult = runShlOperation(shift, value); + final Bytes optimizedResult = runShlOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as( + "SHL mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 5000) + void property_shlOptimized_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 Bytes originalResult = runShlOperation(shiftBytes, value); + final Bytes optimizedResult = runShlOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SHL mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 1000) + void property_shlOptimized_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes originalResult = runShlOperation(shiftBytes, value); + final Bytes optimizedResult = runShlOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SHL overflow mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region SHR Property Tests + + @Property(tries = 10000) + void property_shrOptimized_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 Bytes originalResult = runShrOperation(shift, value); + final Bytes optimizedResult = runShrOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as( + "SHR mismatch for shift=%s, value=%s", + shift.toHexString(), Bytes32.leftPad(value).toHexString()) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 5000) + void property_shrOptimized_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 Bytes originalResult = runShrOperation(shiftBytes, value); + final Bytes optimizedResult = runShrOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SHR mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + @Property(tries = 1000) + void property_shrOptimized_matchesOriginal_overflowShifts( + @ForAll("values1to32") final byte[] valueBytes, @ForAll("overflowShifts") final int shift) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shiftBytes = intToMinimalBytes(shift); + + final Bytes originalResult = runShrOperation(shiftBytes, value); + final Bytes optimizedResult = runShrOperationOptimized(shiftBytes, value); + + assertThat(Bytes32.leftPad(optimizedResult)) + .as("SHR overflow mismatch for shift=%d", shift) + .isEqualTo(Bytes32.leftPad(originalResult)); + } + + // endregion + + // region Edge Case Tests + + @Property(tries = 1000) + void property_shl_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes originalResult = runShlOperation(shift, value); + final Bytes optimizedResult = runShlOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.leftPad(value)); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 1000) + void property_shr_shiftByZero_returnsOriginalValue( + @ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes shift = Bytes.of(0); + + final Bytes originalResult = runShrOperation(shift, value); + final Bytes optimizedResult = runShrOperationOptimized(shift, value); + + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.leftPad(value)); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.leftPad(value)); + } + + @Property(tries = 500) + void property_shl_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes originalResult = runShlOperation(largeShift, value); + final Bytes optimizedResult = runShlOperationOptimized(largeShift, value); + + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.ZERO); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.ZERO); + } + + @Property(tries = 500) + void property_shr_largeShift_returnsZero(@ForAll("values1to32") final byte[] valueBytes) { + + final Bytes value = Bytes.wrap(valueBytes); + final Bytes largeShift = Bytes.fromHexString("0x010000000000"); + + final Bytes originalResult = runShrOperation(largeShift, value); + final Bytes optimizedResult = runShrOperationOptimized(largeShift, value); + + assertThat(Bytes32.leftPad(optimizedResult)).isEqualTo(Bytes32.ZERO); + assertThat(Bytes32.leftPad(originalResult)).isEqualTo(Bytes32.ZERO); + } + + // endregion + + // region Helper Methods + + private Bytes runShlOperation(final Bytes shift, final Bytes value) { + return runOperation(shift, value, ShlOperation::staticOperation); + } + + private Bytes runShlOperationOptimized(final Bytes shift, final Bytes value) { + return runOperation(shift, value, ShlOperationOptimized::staticOperation); + } + + private Bytes runShrOperation(final Bytes shift, final Bytes value) { + return runOperation(shift, value, ShrOperation::staticOperation); + } + + private Bytes runShrOperationOptimized(final Bytes shift, final Bytes value) { + return runOperation(shift, value, ShrOperationOptimized::staticOperation); + } + + @FunctionalInterface + interface OperationExecutor { + Operation.OperationResult execute(MessageFrame frame); + } + + private Bytes runOperation( + final Bytes shift, final Bytes value, final OperationExecutor 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]; + } + + 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 +}