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(); + } +}