diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PopOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PopOperationBenchmarkV2.java new file mode 100644 index 00000000000..c013103b76d --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PopOperationBenchmarkV2.java @@ -0,0 +1,112 @@ +/* + * 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.mockito.Mockito.mock; + +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.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.PopOperation; +import org.hyperledger.besu.evm.v2.operation.PopOperationV2; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.concurrent.TimeUnit; + +import org.apache.tuweni.bytes.Bytes; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * JMH benchmark comparing v1 and v2 POP operations. + * + *
Each iteration pushes a value then pops it, measuring the pop cost. The push is needed to + * ensure the stack is non-empty. + */ +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class PopOperationBenchmarkV2 { + + private static final Bytes STACK_VALUE = + Bytes.fromHexString("0x3232323232323232323232323232323232323232323232323232323232323232"); + + private MessageFrame v1Frame; + private MessageFrame v2Frame; + + @Setup(Level.Iteration) + public void setUp() { + v1Frame = createFrame(false); + v2Frame = createFrame(true); + } + + @Benchmark + public void v1Pop(final Blackhole blackhole) { + v1Frame.pushStackItem(STACK_VALUE); + blackhole.consume(PopOperation.staticOperation(v1Frame)); + } + + @Benchmark + public void v2Pop(final Blackhole blackhole) { + // Push one item onto v2 stack so POP has something to remove + final long[] s = v2Frame.stackDataV2(); + final int top = v2Frame.stackTopV2(); + final int dst = top << 2; + s[dst] = 0x3232323232323232L; + s[dst + 1] = 0x3232323232323232L; + s[dst + 2] = 0x3232323232323232L; + s[dst + 3] = 0x3232323232323232L; + v2Frame.setTopV2(top + 1); + + blackhole.consume(PopOperationV2.staticOperation(v2Frame)); + } + + private static MessageFrame createFrame(final boolean enableV2) { + return MessageFrame.builder() + .enableEvmV2(enableV2) + .worldUpdater(mock(WorldUpdater.class)) + .originator(Address.ZERO) + .gasPrice(Wei.ONE) + .blobGasPrice(Wei.ONE) + .blockValues(mock(BlockValues.class)) + .miningBeneficiary(Address.ZERO) + .blockHashLookup((__, ___) -> Hash.ZERO) + .type(MessageFrame.Type.MESSAGE_CALL) + .initialGas(Long.MAX_VALUE) + .address(Address.ZERO) + .contract(Address.ZERO) + .inputData(Bytes.EMPTY) + .sender(Address.ZERO) + .value(Wei.ZERO) + .apparentValue(Wei.ZERO) + .code(Code.EMPTY_CODE) + .completer(__ -> {}) + .build(); + } +} diff --git a/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PushOperationBenchmarkV2.java b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PushOperationBenchmarkV2.java new file mode 100644 index 00000000000..d33b439daa3 --- /dev/null +++ b/ethereum/core/src/jmh/java/org/hyperledger/besu/ethereum/vm/operations/v2/PushOperationBenchmarkV2.java @@ -0,0 +1,135 @@ +/* + * 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.mockito.Mockito.mock; + +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.frame.BlockValues; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.operation.PushOperation; +import org.hyperledger.besu.evm.v2.operation.PushOperationV2; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.apache.tuweni.bytes.Bytes; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * JMH benchmark comparing v1 and v2 PUSH operations. + * + *
Each iteration pushes a value from random bytecode onto the stack, then resets the stack + * pointer to avoid overflow. Parameterized by push size to cover different limb-filling code paths. + */ +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class PushOperationBenchmarkV2 { + + private static final int SAMPLE_SIZE = 30_000; + + @Param({"1", "2", "8", "16", "32"}) + private int pushSize; + + private byte[][] codePool; + private int index; + + private MessageFrame v1Frame; + private MessageFrame v2Frame; + + @Setup(Level.Iteration) + public void setUp() { + v1Frame = createFrame(false); + v2Frame = createFrame(true); + + final Random random = new Random(); + codePool = new byte[SAMPLE_SIZE][]; + for (int i = 0; i < SAMPLE_SIZE; i++) { + // bytecode: opcode byte + pushSize random immediate bytes + final byte[] code = new byte[1 + pushSize]; + code[0] = (byte) (0x5F + pushSize); // PUSH opcode + for (int j = 1; j <= pushSize; j++) { + code[j] = (byte) random.nextInt(256); + } + codePool[i] = code; + } + index = 0; + } + + @Benchmark + public void v1Push(final Blackhole blackhole) { + final byte[] code = codePool[index]; + blackhole.consume(PushOperation.staticOperation(v1Frame, code, 0, pushSize)); + v1Frame.popStackItem(); + index = (index + 1) % SAMPLE_SIZE; + } + + @Benchmark + public void v2Push(final Blackhole blackhole) { + final byte[] code = codePool[index]; + // Use specialized paths for PUSH1/PUSH2, matching the EVM dispatch + blackhole.consume( + switch (pushSize) { + case 1 -> PushOperationV2.staticPush1(v2Frame, v2Frame.stackDataV2(), code, 0); + case 2 -> PushOperationV2.staticPush2(v2Frame, v2Frame.stackDataV2(), code, 0); + default -> + PushOperationV2.staticOperation(v2Frame, v2Frame.stackDataV2(), code, 0, pushSize); + }); + v2Frame.setTopV2(v2Frame.stackTopV2() - 1); // reset stack + v2Frame.setPC(0); // reset PC + index = (index + 1) % SAMPLE_SIZE; + } + + private static MessageFrame createFrame(final boolean enableV2) { + return MessageFrame.builder() + .enableEvmV2(enableV2) + .worldUpdater(mock(WorldUpdater.class)) + .originator(Address.ZERO) + .gasPrice(Wei.ONE) + .blobGasPrice(Wei.ONE) + .blockValues(mock(BlockValues.class)) + .miningBeneficiary(Address.ZERO) + .blockHashLookup((__, ___) -> Hash.ZERO) + .type(MessageFrame.Type.MESSAGE_CALL) + .initialGas(Long.MAX_VALUE) + .address(Address.ZERO) + .contract(Address.ZERO) + .inputData(Bytes.EMPTY) + .sender(Address.ZERO) + .value(Wei.ZERO) + .apparentValue(Wei.ZERO) + .code(Code.EMPTY_CODE) + .completer(__ -> {}) + .build(); + } +} 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 4384c5e3320..ffc26c79362 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -88,6 +88,9 @@ import org.hyperledger.besu.evm.operation.XorOperationOptimized; import org.hyperledger.besu.evm.tracing.OperationTracer; import org.hyperledger.besu.evm.v2.operation.AddOperationV2; +import org.hyperledger.besu.evm.v2.operation.PopOperationV2; +import org.hyperledger.besu.evm.v2.operation.Push0OperationV2; +import org.hyperledger.besu.evm.v2.operation.PushOperationV2; import org.hyperledger.besu.evm.v2.operation.SarOperationV2; import org.hyperledger.besu.evm.v2.operation.ShlOperationV2; import org.hyperledger.besu.evm.v2.operation.ShrOperationV2; @@ -500,6 +503,47 @@ private void runToHaltV2(final MessageFrame frame, final OperationTracer tracing enableConstantinople ? SarOperationV2.staticOperation(frame, frame.stackDataV2()) : InvalidOperation.invalidOperationResult(opcode); + case 0x50 -> PopOperationV2.staticOperation(frame); + case 0x5f -> + enableShanghai + ? Push0OperationV2.staticOperation(frame, frame.stackDataV2()) + : InvalidOperation.invalidOperationResult(opcode); + case 0x60 -> // PUSH1 — specialized for the most frequent opcode + PushOperationV2.staticPush1(frame, frame.stackDataV2(), code, pc); + case 0x61 -> // PUSH2 — specialized for jump destinations + PushOperationV2.staticPush2(frame, frame.stackDataV2(), code, pc); + case 0x62, // PUSH3-32 + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6a, + 0x6b, + 0x6c, + 0x6d, + 0x6e, + 0x6f, + 0x70, + 0x71, + 0x72, + 0x73, + 0x74, + 0x75, + 0x76, + 0x77, + 0x78, + 0x79, + 0x7a, + 0x7b, + 0x7c, + 0x7d, + 0x7e, + 0x7f -> + PushOperationV2.staticOperation( + frame, frame.stackDataV2(), code, pc, opcode - PushOperationV2.PUSH_BASE); // 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/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index a90da960e38..ba688760359 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -518,6 +518,16 @@ public boolean stackHasItems(final int n) { return stackTopV2 >= n; } + /** + * Returns true if the v2 stack has space for at least {@code n} more items. + * + * @param n the number of items to push + * @return true if the stack can accommodate n more items + */ + public boolean stackHasSpace(final int n) { + return stackTopV2 + n <= getMaxStackSize(); + } + // --------------------------------------------------------------------------- // endregion diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java index 6a9f808f3dc..eb2b0587ea1 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/StackArithmetic.java @@ -27,6 +27,11 @@ public class StackArithmetic { /** Utility class — not instantiable. */ private StackArithmetic() {} + /** Big-endian VarHandle for reading 8 bytes from a byte[] as a long. */ + private static final java.lang.invoke.VarHandle LONG_BE = + java.lang.invoke.MethodHandles.byteArrayViewVarHandle( + long[].class, java.nio.ByteOrder.BIG_ENDIAN); + // region SHL (Shift Left) // --------------------------------------------------------------------------- @@ -321,4 +326,215 @@ private static long shiftRightWord(final long value, final long prevValue, final } // endregion + + // region Stack Manipulation (PUSH / POP) + // --------------------------------------------------------------------------- + + /** + * Specialized PUSH1: reads a single byte from bytecode and pushes it as a 256-bit value. Avoids + * the generic pushFromBytes dispatch for the most frequently executed opcode (~14% of all + * instructions). Mirrors Geth's dedicated opPush1 function. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @param code the raw bytecode array + * @param pc the current program counter (pointing at the PUSH1 opcode) + * @return the new stack-top + */ + public static int push1(final long[] stack, final int top, final byte[] code, final int pc) { + final int dst = top << 2; + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + stack[dst + 3] = (pc + 1 < code.length) ? (code[pc + 1] & 0xFFL) : 0L; + return top + 1; + } + + /** + * Specialized PUSH2: reads two bytes from bytecode and pushes them as a 256-bit value. The second + * most common PUSH variant, used for jump destinations. Mirrors Geth's dedicated opPush2 + * function. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @param code the raw bytecode array + * @param pc the current program counter (pointing at the PUSH2 opcode) + * @return the new stack-top + */ + public static int push2(final long[] stack, final int top, final byte[] code, final int pc) { + final int dst = top << 2; + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + final int start = pc + 1; + if (start + 1 < code.length) { + stack[dst + 3] = (code[start] & 0xFFL) << 8 | (code[start + 1] & 0xFFL); + } else if (start < code.length) { + stack[dst + 3] = (code[start] & 0xFFL) << 8; + } else { + stack[dst + 3] = 0; + } + return top + 1; + } + + /** + * Pushes a zero-valued 256-bit word onto the stack. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @return the new stack-top + */ + public static int pushZero(final long[] stack, final int top) { + final int dst = top << 2; + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + stack[dst + 3] = 0; + return top + 1; + } + + /** + * Reads {@code len} bytes from the bytecode at position {@code start} and pushes them as a + * right-aligned (big-endian) 256-bit value onto the stack. + * + *
The fast path branches on the number of bytes to minimize work: values fitting in one limb + * (1-8 bytes) write only limb 3, two-limb values (9-16) write limbs 2-3, and so on. A slow path + * handles truncated bytecode at the end of the code array. + * + * @param stack the flat limb array + * @param top current stack-top (item count) + * @param code the raw bytecode array + * @param start the index in code of the first byte to read + * @param len the number of bytes to read (1..32) + * @return the new stack-top + */ + public static int pushFromBytes( + final long[] stack, final int top, final byte[] code, final int start, final int len) { + final int dst = top << 2; + + // Out-of-bounds: push zero + if (start >= code.length) { + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + stack[dst + 3] = 0; + return top + 1; + } + final int copyLen = Math.min(len, code.length - start); + + if (copyLen == len) { + // Fast path: all bytes available in the code array. + // Each branch writes exactly the limbs it needs and zeros the rest, + // avoiding a separate zeroing pass. + if (len <= 8) { + // Fits in one limb (u0) + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + stack[dst + 3] = buildLong(code, start, len); + } else if (len <= 16) { + // Two limbs (u1, u0) + final int hiLen = len - 8; + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = buildLong(code, start, hiLen); + stack[dst + 3] = bytesToLong(code, start + hiLen); + } else if (len <= 24) { + // Three limbs (u2, u1, u0) + final int hiLen = len - 16; + stack[dst] = 0; + stack[dst + 1] = buildLong(code, start, hiLen); + stack[dst + 2] = bytesToLong(code, start + hiLen); + stack[dst + 3] = bytesToLong(code, start + hiLen + 8); + } else if (len == 32) { + // All four limbs full — use VarHandle for all reads, no buildLong loop + stack[dst] = bytesToLong(code, start); + stack[dst + 1] = bytesToLong(code, start + 8); + stack[dst + 2] = bytesToLong(code, start + 16); + stack[dst + 3] = bytesToLong(code, start + 24); + } else { + // 25-31 bytes: partial first limb + three full limbs + final int hiLen = len - 24; + stack[dst] = buildLong(code, start, hiLen); + stack[dst + 1] = bytesToLong(code, start + hiLen); + stack[dst + 2] = bytesToLong(code, start + hiLen + 8); + stack[dst + 3] = bytesToLong(code, start + hiLen + 16); + } + } else { + // Slow path: bytecode is truncated (partial push at end of code). + // Zero all limbs first, then place available bytes right-aligned + // within the 256-bit value, padding with zeros on the right. + stack[dst] = 0; + stack[dst + 1] = 0; + stack[dst + 2] = 0; + stack[dst + 3] = 0; + int bytePos = len - 1; + for (int i = 0; i < copyLen; i++) { + final int limbOffset = 3 - (bytePos >> 3); + final int shift = (bytePos & 7) << 3; + stack[dst + limbOffset] |= (code[start + i] & 0xFFL) << shift; + bytePos--; + } + } + return top + 1; + } + + /** + * Builds a {@code long} from 1..8 bytes in big-endian order. Uses a switch on length to avoid + * loop overhead, following the same pattern as Geth's SetBytes1..SetBytes8 specializations in + * holiman/uint256. + * + * @param src the source byte array + * @param off the start offset in src + * @param len the number of bytes (1..8) + * @return the assembled long value + */ + private static long buildLong(final byte[] src, final int off, final int len) { + return switch (len) { + case 1 -> src[off] & 0xFFL; + case 2 -> (src[off] & 0xFFL) << 8 | (src[off + 1] & 0xFFL); + case 3 -> (src[off] & 0xFFL) << 16 | (src[off + 1] & 0xFFL) << 8 | (src[off + 2] & 0xFFL); + case 4 -> + (src[off] & 0xFFL) << 24 + | (src[off + 1] & 0xFFL) << 16 + | (src[off + 2] & 0xFFL) << 8 + | (src[off + 3] & 0xFFL); + case 5 -> + (src[off] & 0xFFL) << 32 + | (src[off + 1] & 0xFFL) << 24 + | (src[off + 2] & 0xFFL) << 16 + | (src[off + 3] & 0xFFL) << 8 + | (src[off + 4] & 0xFFL); + case 6 -> + (src[off] & 0xFFL) << 40 + | (src[off + 1] & 0xFFL) << 32 + | (src[off + 2] & 0xFFL) << 24 + | (src[off + 3] & 0xFFL) << 16 + | (src[off + 4] & 0xFFL) << 8 + | (src[off + 5] & 0xFFL); + case 7 -> + (src[off] & 0xFFL) << 48 + | (src[off + 1] & 0xFFL) << 40 + | (src[off + 2] & 0xFFL) << 32 + | (src[off + 3] & 0xFFL) << 24 + | (src[off + 4] & 0xFFL) << 16 + | (src[off + 5] & 0xFFL) << 8 + | (src[off + 6] & 0xFFL); + case 8 -> bytesToLong(src, off); + default -> throw new IllegalArgumentException("buildLong len must be 1..8, got " + len); + }; + } + + /** + * Reads exactly 8 bytes in big-endian order as a {@code long} using a VarHandle for performance. + * + * @param src the source byte array + * @param off the start offset + * @return the 8-byte long value + */ + private static long bytesToLong(final byte[] src, final int off) { + return (long) LONG_BE.get(src, off); + } + + // endregion } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java index 0480e77b067..b00cb8657d8 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AbstractFixedCostOperationV2.java @@ -29,6 +29,10 @@ abstract class AbstractFixedCostOperationV2 extends AbstractOperation { static final OperationResult UNDERFLOW_RESPONSE = new OperationResult(0L, ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); + /** Shared overflow response for static operation methods. */ + static final OperationResult OVERFLOW_RESPONSE = + new OperationResult(0L, ExceptionalHaltReason.TOO_MANY_STACK_ITEMS); + /** The Success response. */ protected final OperationResult successResponse; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2.java new file mode 100644 index 00000000000..1ec54132955 --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** EVM v2 POP operation. Removes the top item from the long[] stack. */ +public class PopOperationV2 extends AbstractFixedCostOperationV2 { + + /** The Pop operation success result. */ + static final OperationResult popSuccess = new OperationResult(2, null); + + /** + * Instantiates a new Pop operation. + * + * @param gasCalculator the gas calculator + */ + public PopOperationV2(final GasCalculator gasCalculator) { + super(0x50, "POP", 1, 0, gasCalculator, gasCalculator.getBaseTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame); + } + + /** + * Performs Pop operation on the v2 stack. + * + * @param frame the frame + * @return the operation result + */ + public static OperationResult staticOperation(final MessageFrame frame) { + if (!frame.stackHasItems(1)) return UNDERFLOW_RESPONSE; + frame.setTopV2(frame.stackTopV2() - 1); + return popSuccess; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2.java new file mode 100644 index 00000000000..3663079c42c --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2.java @@ -0,0 +1,58 @@ +/* + * 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.hyperledger.besu.evm.v2.operation.PushOperationV2.PUSH_BASE; + +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; + +/** EVM v2 PUSH0 operation. Pushes a zero-valued 256-bit word onto the long[] stack. */ +public class Push0OperationV2 extends AbstractFixedCostOperationV2 { + + /** The Push0 operation success result. */ + static final OperationResult push0Success = new OperationResult(2, null); + + /** + * Instantiates a new Push0 operation. + * + * @param gasCalculator the gas calculator + */ + public Push0OperationV2(final GasCalculator gasCalculator) { + super(PUSH_BASE, "PUSH0", 0, 1, gasCalculator, gasCalculator.getBaseTierGasCost()); + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + return staticOperation(frame, frame.stackDataV2()); + } + + /** + * Performs Push0 operation on the v2 stack. + * + * @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.stackHasSpace(1)) return OVERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.pushZero(stack, frame.stackTopV2())); + return push0Success; + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PushOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PushOperationV2.java new file mode 100644 index 00000000000..5b2ca8f9e2b --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/PushOperationV2.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.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; + +/** + * EVM v2 PUSH1-PUSH32 operation. Reads immediate bytes from bytecode and pushes them as a 256-bit + * value onto the long[] stack. + */ +public class PushOperationV2 extends AbstractFixedCostOperationV2 { + + /** The constant PUSH_BASE (0x5F). PUSH1 = PUSH_BASE + 1, PUSH32 = PUSH_BASE + 32. */ + public static final int PUSH_BASE = 0x5F; + + /** The Push operation success result. */ + static final OperationResult pushSuccess = new OperationResult(3, null); + + private final int length; + + /** + * Instantiates a new Push operation. + * + * @param length the number of immediate bytes (1..32) + * @param gasCalculator the gas calculator + */ + public PushOperationV2(final int length, final GasCalculator gasCalculator) { + super( + PUSH_BASE + length, + "PUSH" + length, + 0, + 1, + gasCalculator, + gasCalculator.getVeryLowTierGasCost()); + this.length = length; + } + + @Override + public Operation.OperationResult executeFixedCostOperation( + final MessageFrame frame, final EVM evm) { + final byte[] code = frame.getCode().getBytes().toArrayUnsafe(); + return staticOperation(frame, frame.stackDataV2(), code, frame.getPC(), length); + } + + /** + * Performs Push operation on the v2 stack. Reads {@code pushSize} bytes from the bytecode + * starting after the current opcode and pushes them as a right-aligned 256-bit value. + * + * @param frame the frame + * @param stack the v2 operand stack ({@code long[]} in big-endian limb order) + * @param code the raw bytecode array + * @param pc the current program counter (pointing at the PUSH opcode) + * @param pushSize the number of immediate bytes to read (1..32) + * @return the operation result + */ + public static OperationResult staticOperation( + final MessageFrame frame, + final long[] stack, + final byte[] code, + final int pc, + final int pushSize) { + if (!frame.stackHasSpace(1)) return OVERFLOW_RESPONSE; + frame.setTopV2( + StackArithmetic.pushFromBytes(stack, frame.stackTopV2(), code, pc + 1, pushSize)); + frame.setPC(pc + pushSize); + return pushSuccess; + } + + /** + * Specialized PUSH1 operation. Avoids the generic pushFromBytes dispatch for the single most + * frequently executed opcode. + * + * @param frame the frame + * @param stack the v2 operand stack + * @param code the raw bytecode array + * @param pc the current program counter + * @return the operation result + */ + public static OperationResult staticPush1( + final MessageFrame frame, final long[] stack, final byte[] code, final int pc) { + if (!frame.stackHasSpace(1)) return OVERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.push1(stack, frame.stackTopV2(), code, pc)); + frame.setPC(pc + 1); + return pushSuccess; + } + + /** + * Specialized PUSH2 operation. Avoids the generic pushFromBytes dispatch for the second most + * common PUSH variant, used for jump destinations. + * + * @param frame the frame + * @param stack the v2 operand stack + * @param code the raw bytecode array + * @param pc the current program counter + * @return the operation result + */ + public static OperationResult staticPush2( + final MessageFrame frame, final long[] stack, final byte[] code, final int pc) { + if (!frame.stackHasSpace(1)) return OVERFLOW_RESPONSE; + frame.setTopV2(StackArithmetic.push2(stack, frame.stackTopV2(), code, pc)); + frame.setPC(pc + 2); + return pushSuccess; + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2Test.java new file mode 100644 index 00000000000..0dd434e52f8 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/PopOperationV2Test.java @@ -0,0 +1,80 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.v2.operation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +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.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; + +class PopOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final PopOperationV2 operation = new PopOperationV2(gasCalculator); + + @Test + void popRemovesTopItem() { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient("0x01")) + .pushStackItem(Bytes32.fromHexStringLenient("0x02")) + .build(); + assertThat(frame.stackTopV2()).isEqualTo(2); + + final OperationResult result = operation.execute(frame, null); + + assertThat(result.getHaltReason()).isNull(); + assertThat(result.getGasCost()).isEqualTo(2L); + assertThat(frame.stackTopV2()).isEqualTo(1); + } + + @Test + void popOnEmptyStackReturnsUnderflow() { + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + assertThat(frame.stackTopV2()).isEqualTo(0); + + final OperationResult result = PopOperationV2.staticOperation(frame); + + assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); + } + + @Test + void popStaticOperationDecrementsTop() { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexStringLenient("0xABCD")) + .build(); + assertThat(frame.stackTopV2()).isEqualTo(1); + + final OperationResult result = PopOperationV2.staticOperation(frame); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(0); + } + + @Test + void dryRunDetector() { + assertThat(true) + .withFailMessage("This test is here so gradle --dry-run executes this class") + .isTrue(); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2Test.java new file mode 100644 index 00000000000..affabfedf81 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/Push0OperationV2Test.java @@ -0,0 +1,71 @@ +/* + * 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.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import org.junit.jupiter.api.Test; + +class Push0OperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final Push0OperationV2 operation = new Push0OperationV2(gasCalculator); + + @Test + void push0PushesZeroOntoStack() { + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + assertThat(frame.stackTopV2()).isEqualTo(0); + + final OperationResult result = operation.execute(frame, null); + + assertThat(result.getHaltReason()).isNull(); + assertThat(result.getGasCost()).isEqualTo(2L); + assertThat(frame.stackTopV2()).isEqualTo(1); + assertThat(getV2StackItem(frame, 0)).isEqualTo(UInt256.ZERO); + } + + @Test + void push0AllLimbsAreZero() { + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + Push0OperationV2.staticOperation(frame, frame.stackDataV2()); + + final long[] s = frame.stackDataV2(); + assertThat(s[0]).isEqualTo(0L); + assertThat(s[1]).isEqualTo(0L); + assertThat(s[2]).isEqualTo(0L); + assertThat(s[3]).isEqualTo(0L); + } + + @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/PushOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/PushOperationV2Test.java new file mode 100644 index 00000000000..988f82d347d --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/PushOperationV2Test.java @@ -0,0 +1,197 @@ +/* + * 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.Code; +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.operation.Operation.OperationResult; +import org.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +class PushOperationV2Test { + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final PushOperationV2 operation = new PushOperationV2(1, gasCalculator); + + @Test + void push1ViaExecute() { + // Test through the instance execute() path, matching the pattern from ShlOperationV2Test + final byte[] codeBytes = new byte[] {0x60, (byte) 0xAB}; + final MessageFrame frame = + new TestMessageFrameBuilderV2().code(new Code(Bytes.wrap(codeBytes))).build(); + + final OperationResult result = operation.execute(frame, null); + + assertThat(result.getHaltReason()).isNull(); + assertThat(result.getGasCost()).isEqualTo(3L); + assertThat(frame.stackTopV2()).isEqualTo(1); + assertThat(getV2StackItem(frame, 0)).isEqualTo(new UInt256(0, 0, 0, 0xABL)); + } + + @Test + void push1SingleByte() { + // PUSH1 0xAB — bytecode: [0x60, 0xAB] + final byte[] code = new byte[] {0x60, (byte) 0xAB}; + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 1); + + assertThat(result.getHaltReason()).isNull(); + assertThat(result.getGasCost()).isEqualTo(3L); + assertThat(frame.stackTopV2()).isEqualTo(1); + assertThat(getV2StackItem(frame, 0)).isEqualTo(new UInt256(0, 0, 0, 0xABL)); + assertThat(frame.getPC()).isEqualTo(1); // pc advanced past immediate byte + } + + @Test + void push8FullLimb() { + // PUSH8 0x0102030405060708 — fills exactly one limb + final byte[] code = new byte[] {0x67, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 8); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(1); + assertThat(getV2StackItem(frame, 0)).isEqualTo(new UInt256(0, 0, 0, 0x0102030405060708L)); + } + + @Test + void push16TwoLimbs() { + // PUSH16 with 16 bytes → fills limbs 2 and 3 + final byte[] code = new byte[17]; + code[0] = 0x6f; // PUSH16 opcode + for (int i = 1; i <= 16; i++) { + code[i] = (byte) i; + } + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 16); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(1); + final UInt256 item = getV2StackItem(frame, 0); + assertThat(item.u3()).isEqualTo(0L); + assertThat(item.u2()).isEqualTo(0L); + assertThat(item.u1()).isEqualTo(0x0102030405060708L); + assertThat(item.u0()).isEqualTo(0x090a0b0c0d0e0f10L); + } + + @Test + void push32FourLimbs() { + // PUSH32 with 32 bytes → fills all 4 limbs + final byte[] code = new byte[33]; + code[0] = 0x7f; // PUSH32 opcode + for (int i = 1; i <= 32; i++) { + code[i] = (byte) i; + } + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 32); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(1); + final UInt256 item = getV2StackItem(frame, 0); + assertThat(item.u3()).isEqualTo(0x0102030405060708L); + assertThat(item.u2()).isEqualTo(0x090a0b0c0d0e0f10L); + assertThat(item.u1()).isEqualTo(0x1112131415161718L); + assertThat(item.u0()).isEqualTo(0x191a1b1c1d1e1f20L); + } + + @Test + void push2TruncatedBytecodeRightPads() { + // PUSH2 but only 1 byte available after opcode → right-padded with zero + final byte[] code = new byte[] {0x61, (byte) 0xFF}; + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 2); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(1); + // 0xFF with 1 byte padding → 0xFF00 + assertThat(getV2StackItem(frame, 0)).isEqualTo(new UInt256(0, 0, 0, 0xFF00L)); + } + + @Test + void pushBeyondCodeLengthPushesZero() { + // Code is just the opcode, no immediate bytes + final byte[] code = new byte[] {0x60}; + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 1); + + assertThat(result.getHaltReason()).isNull(); + assertThat(frame.stackTopV2()).isEqualTo(1); + assertThat(getV2StackItem(frame, 0)).isEqualTo(UInt256.ZERO); + } + + @Test + void pcAdvancedByPushSize() { + final byte[] code = new byte[] {0x63, 0x01, 0x02, 0x03, 0x04}; // PUSH4 + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 4); + + // PC should be set to pushSize (4), the dispatch loop adds +1 for the opcode + assertThat(frame.getPC()).isEqualTo(4); + } + + @Test + void push24ThreeLimbs() { + // PUSH24 with 24 bytes → fills limbs 1, 2, and 3 + final byte[] code = new byte[25]; + code[0] = 0x77; // PUSH24 opcode + for (int i = 1; i <= 24; i++) { + code[i] = (byte) i; + } + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + + final OperationResult result = + PushOperationV2.staticOperation(frame, frame.stackDataV2(), code, 0, 24); + + assertThat(result.getHaltReason()).isNull(); + final UInt256 item = getV2StackItem(frame, 0); + assertThat(item.u3()).isEqualTo(0L); + assertThat(item.u2()).isEqualTo(0x0102030405060708L); + assertThat(item.u1()).isEqualTo(0x090a0b0c0d0e0f10L); + assertThat(item.u0()).isEqualTo(0x1112131415161718L); + } + + @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]); + } +}