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..31e64b2962f
--- /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. */
+ 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..e617026a236
--- /dev/null
+++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/SarOperationBenchmarkV2.java
@@ -0,0 +1,156 @@
+/*
+ * 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 org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.operation.Operation;
+import org.hyperledger.besu.evm.v2.operation.SarOperationV2;
+
+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) {
+ 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..117f8148036
--- /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 AbstractShiftOperationBenchmarkV2 {
+
+ @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..fa3dd952609
--- /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 AbstractShiftOperationBenchmarkV2 {
+
+ @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..6a9f808f3dc
--- /dev/null
+++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java
@@ -0,0 +1,324 @@
+/*
+ * 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 {
+
+ /** Utility class — not instantiable. */
+ private StackArithmetic() {}
+
+ // region SHL (Shift Left)
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Performs EVM SHL (shift left) on the two top stack items.
+ *
+ *
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)
+ * @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 (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) stack[shiftOffset + 3];
+ shiftLeftInPlace(stack, valueOffset, shift);
+ return top - 1;
+ }
+
+ /**
+ * 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;
+ 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:
+ w0 = shiftLeftWord(w0, w1, bitShift);
+ w1 = shiftLeftWord(w1, w2, bitShift);
+ w2 = shiftLeftWord(w2, w3, bitShift);
+ w3 = shiftLeftWord(w3, 0, bitShift);
+ break;
+ case 1:
+ w0 = shiftLeftWord(w1, w2, bitShift);
+ w1 = shiftLeftWord(w2, w3, bitShift);
+ w2 = shiftLeftWord(w3, 0, bitShift);
+ w3 = 0;
+ break;
+ case 2:
+ w0 = shiftLeftWord(w2, w3, bitShift);
+ w1 = shiftLeftWord(w3, 0, bitShift);
+ w2 = 0;
+ w3 = 0;
+ break;
+ case 3:
+ w0 = shiftLeftWord(w3, 0, bitShift);
+ w1 = 0;
+ w2 = 0;
+ w3 = 0;
+ break;
+ }
+ stack[valueOffset] = w0;
+ stack[valueOffset + 1] = w1;
+ stack[valueOffset + 2] = w2;
+ stack[valueOffset + 3] = w3;
+ }
+
+ /**
+ * 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));
+ }
+
+ // endregion
+
+ // region SHR (Shift Right)
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Performs EVM SHR (logical shift right) on the two top stack items.
+ *
+ *
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)
+ * @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) stack[shiftOffset + 3];
+ shiftRightInPlace(stack, valueOffset, shift);
+ return top - 1;
+ }
+
+ /**
+ * Logically right-shifts a 256-bit value in place by 1..255 bits, zero-filling from the left.
+ *
+ * @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[] stack, final int valueOffset, final int shift) {
+ if (shift == 0) return;
+ 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:
+ w3 = shiftRightWord(w3, w2, bitShift);
+ w2 = shiftRightWord(w2, w1, bitShift);
+ w1 = shiftRightWord(w1, w0, bitShift);
+ w0 = shiftRightWord(w0, 0, bitShift);
+ break;
+ case 1:
+ w3 = shiftRightWord(w2, w1, bitShift);
+ w2 = shiftRightWord(w1, w0, bitShift);
+ w1 = shiftRightWord(w0, 0, bitShift);
+ w0 = 0;
+ break;
+ case 2:
+ w3 = shiftRightWord(w1, w0, bitShift);
+ w2 = shiftRightWord(w0, 0, bitShift);
+ w1 = 0;
+ w0 = 0;
+ break;
+ case 3:
+ w3 = shiftRightWord(w0, 0, bitShift);
+ w2 = 0;
+ w1 = 0;
+ w0 = 0;
+ break;
+ }
+ stack[valueOffset] = w0;
+ stack[valueOffset + 1] = w1;
+ stack[valueOffset + 2] = w2;
+ stack[valueOffset + 3] = w3;
+ }
+
+ // endregion
+
+ // region SAR (Shift Arithmetic Right)
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Performs EVM SAR (arithmetic shift right) on the two top stack items.
+ *
+ *
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)
+ * @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;
+ stack[valueOffset] = fill;
+ stack[valueOffset + 1] = fill;
+ stack[valueOffset + 2] = fill;
+ stack[valueOffset + 3] = fill;
+ return top - 1;
+ }
+ int shift = (int) stack[shiftOffset + 3];
+ sarInPlace(stack, valueOffset, shift, negative);
+ return top - 1;
+ }
+
+ /**
+ * 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[] stack, final int valueOffset, final int shift, final boolean negative) {
+ if (shift == 0) return;
+ long w0 = stack[valueOffset],
+ w1 = stack[valueOffset + 1],
+ 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:
+ w3 = shiftRightWord(w3, w2, bitShift);
+ w2 = shiftRightWord(w2, w1, bitShift);
+ w1 = shiftRightWord(w1, w0, bitShift);
+ w0 = shiftRightWord(w0, fill, bitShift);
+ break;
+ case 1:
+ w3 = shiftRightWord(w2, w1, bitShift);
+ w2 = shiftRightWord(w1, w0, bitShift);
+ w1 = shiftRightWord(w0, fill, bitShift);
+ w0 = fill;
+ break;
+ case 2:
+ w3 = shiftRightWord(w1, w0, bitShift);
+ w2 = shiftRightWord(w0, fill, bitShift);
+ w1 = fill;
+ w0 = fill;
+ break;
+ case 3:
+ w3 = shiftRightWord(w0, fill, bitShift);
+ w2 = fill;
+ w1 = fill;
+ w0 = fill;
+ break;
+ }
+ stack[valueOffset] = w0;
+ stack[valueOffset + 1] = w1;
+ stack[valueOffset + 2] = w2;
+ stack[valueOffset + 3] = w3;
+ }
+
+ /**
+ * 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));
+ }
+
+ // endregion
+}
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..d488886c5df
--- /dev/null
+++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/SarOperationV2.java
@@ -0,0 +1,56 @@
+/*
+ * 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
+ * @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) {
+ if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE;
+ frame.setTopV2(StackArithmetic.sar(stack, 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..2f765e94269
--- /dev/null
+++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShlOperationV2.java
@@ -0,0 +1,56 @@
+/*
+ * 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
+ * @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) {
+ if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE;
+ 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
new file mode 100644
index 00000000000..71c412a04ab
--- /dev/null
+++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/ShrOperationV2.java
@@ -0,0 +1,56 @@
+/*
+ * 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
+ * @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) {
+ if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE;
+ frame.setTopV2(StackArithmetic.shr(stack, frame.stackTopV2()));
+ return shrSuccess;
+ }
+}
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/ShiftOperationsV2PropertyBasedTest.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java
new file mode 100644
index 00000000000..e9d43c145ed
--- /dev/null
+++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/ShiftOperationsV2PropertyBasedTest.java
@@ -0,0 +1,586 @@
+/*
+ * 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.evm.UInt256;
+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 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
+}
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();
+ }
+}