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 97b7fa76aed..a49e04331ee 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 @@ -22,6 +22,6 @@ public class AddOperationBenchmarkV2 extends BinaryOperationBenchmarkV2 { @Override protected Operation.OperationResult invoke(final MessageFrame frame) { - return AddOperationV2.staticOperation(frame, frame.stackDataV2()); + return AddOperationV2.staticOperation(frame); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java index 2d1ef239f85..bed9b8602e3 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/EVM.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/EVM.java @@ -488,7 +488,7 @@ private void runToHaltV2(final MessageFrame frame, final OperationTracer tracing try { result = switch (opcode) { - case 0x01 -> AddOperationV2.staticOperation(frame, frame.stackDataV2()); + case 0x01 -> AddOperationV2.staticOperation(frame); case 0x09 -> MulModOperationV2.staticOperation(frame); case 0x1b -> enableConstantinople diff --git a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java index 6f3eda6ae74..c130a02d0f0 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/UInt256.java @@ -460,8 +460,6 @@ public UInt256 abs() { * @return The sum. */ public UInt256 add(final UInt256 other) { - if (isZero()) return other; - if (other.isZero()) return this; return adc(other).UInt256Value(); } @@ -883,8 +881,6 @@ private UInt256 shiftDigitsRight() { } private UInt257 adc(final UInt256 other) { - if (isZero()) return new UInt257(false, other); - if (other.isZero()) return new UInt257(false, this); long z0 = u0 + other.u0; long carry = Long.compareUnsigned(z0, u0) < 0 ? 1 : 0; diff --git a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java index b4109d29ad1..c544c830610 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.evm.v2.operation; import org.hyperledger.besu.evm.EVM; +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.operation.Operation; @@ -28,7 +29,6 @@ */ public class AddOperationV2 extends AbstractFixedCostOperationV2 { - @SuppressWarnings("UnusedVariable") private static final Operation.OperationResult ADD_SUCCESS = new Operation.OperationResult(3, null); @@ -44,19 +44,37 @@ public AddOperationV2(final GasCalculator gasCalculator) { @Override public Operation.OperationResult executeFixedCostOperation( final MessageFrame frame, final EVM evm) { - return staticOperation(frame, frame.stackDataV2()); + return staticOperation(frame); } /** * Execute the ADD opcode on the v2 long[] stack. * + *

ADD: stack[top-2] = stack[top-1] + stack[top-2], return top-1. + * * @param frame the message frame - * @param stackData the stack operands as a long[] array * @return the operation result */ - @SuppressWarnings("DoNotCallSuggester") - public static Operation.OperationResult staticOperation( - final MessageFrame frame, final long[] stackData) { - throw new UnsupportedOperationException("ADD operation not yet implemented for evm v2"); + public static Operation.OperationResult staticOperation(final MessageFrame frame) { + if (!frame.stackHasItems(2)) return UNDERFLOW_RESPONSE; + long[] stack = frame.stackDataV2(); + int top = frame.stackTopV2(); + final int aOffset = (top - 1) << 2; + final int bOffset = (top - 2) << 2; + + final UInt256 valueA = + new UInt256(stack[aOffset], stack[aOffset + 1], stack[aOffset + 2], stack[aOffset + 3]); + final UInt256 valueB = + new UInt256(stack[bOffset], stack[bOffset + 1], stack[bOffset + 2], stack[bOffset + 3]); + + final UInt256 r = valueA.add(valueB); + + stack[bOffset] = r.u3(); + stack[bOffset + 1] = r.u2(); + stack[bOffset + 2] = r.u1(); + stack[bOffset + 3] = r.u0(); + + frame.setTopV2(top - 1); + return ADD_SUCCESS; } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2Test.java b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2Test.java new file mode 100644 index 00000000000..2515a054085 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/v2/operation/AddOperationV2Test.java @@ -0,0 +1,145 @@ +/* + * 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.hyperledger.besu.evm.v2.testutils.TestMessageFrameBuilderV2.getV2StackItem; + +import org.hyperledger.besu.evm.UInt256; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.FrontierGasCalculator; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.Operation; +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 AddOperationV2Test { + private final GasCalculator gasCalculator = new FrontierGasCalculator(); + private final AddOperationV2 operation = new AddOperationV2(gasCalculator); + + /** + * Structural test data for add(a, b) = expected. Arithmetic correctness is covered by + * UInt256PropertyBasedTest; these cases verify stack arity, limb-level read/write wiring, and + * 256-bit wrap handling. + * + *

Push order when building the frame: b first (deepest), then a (top). + */ + static Iterable data() { + return List.of( + // (a, b, expected) + // Happy path: low-limb only. + Arguments.of("0x02", "0x03", "0x05"), + // Zero identity with operand ordering: confirms b is read from the deeper slot. + Arguments.of("0x00", "0x03", "0x03"), + // 256-bit wrap: all 4 result limbs written as zero. + Arguments.of( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x01", "0x00"), + // All 4 limbs populated on both inputs and the result (no carries): verifies every limb is + // read and written through stackDataV2(). Four distinct limb values catch any + // limb-index/offset mistakes. + Arguments.of( + "0x1000000000000001200000000000000230000000000000034000000000000004", + "0x1000000000000001100000000000000110000000000000011000000000000001", + "0x2000000000000002300000000000000340000000000000045000000000000005")); + } + + @ParameterizedTest(name = "{index}: add({0}, {1}) = {2}") + @MethodSource("data") + void addOperation(final String a, final String b, final String expectedResult) { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexString(b)) // pushed first → deepest (top-2) + .pushStackItem(Bytes32.fromHexString(a)) // pushed last → top (top-1) + .build(); + assertThat(frame.stackTopV2()).isEqualTo(2); + + final Operation.OperationResult result = operation.execute(frame, null); + + assertThat(result.getHaltReason()).isNull(); + // ADD consumes 2 items and produces 1: net stack change is -1. + assertThat(frame.stackTopV2()).isEqualTo(1); + + final UInt256 expected = + UInt256.fromBytesBE(Bytes32.fromHexString(expectedResult).toArrayUnsafe()); + assertThat(getV2StackItem(frame, 0)).isEqualTo(expected); + } + + @Test + void addOperationUnderflowNoItems() { + final MessageFrame frame = new TestMessageFrameBuilderV2().build(); + assertThat(frame.stackTopV2()).isEqualTo(0); + + final Operation.OperationResult result = AddOperationV2.staticOperation(frame); + + assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); + assertThat(frame.stackTopV2()).isEqualTo(0); + } + + @Test + void addOperationUnderflowOnlyOneItem() { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexString("0x01")) // top-2, missing top-1 + .build(); + assertThat(frame.stackTopV2()).isEqualTo(1); + + final Operation.OperationResult result = AddOperationV2.staticOperation(frame); + + assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS); + assertThat(frame.stackTopV2()).isEqualTo(1); + } + + @Test + void addOperationPreservesDeeperStackItems() { + // Use a distinctive 4-limb value for the untouched deep slot so any accidental write is + // detectable in any limb. + final Bytes32 untouched = + Bytes32.fromHexString("0xdeadbeefdeadbeefcafebabecafebabe0123456789abcdeff1e2d3c4b5a69788"); + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(untouched) // top-3 (untouched by ADD) + .pushStackItem(Bytes32.fromHexString("0x07")) // top-2 (b) + .pushStackItem(Bytes32.fromHexString("0x05")) // top-1 (a) + .build(); + assertThat(frame.stackTopV2()).isEqualTo(3); + + operation.execute(frame, null); + + assertThat(frame.stackTopV2()).isEqualTo(2); + assertThat(getV2StackItem(frame, 0)).isEqualTo(UInt256.fromInt(12)); // 5 + 7 + assertThat(getV2StackItem(frame, 1)).isEqualTo(UInt256.fromBytesBE(untouched.toArrayUnsafe())); + } + + @Test + void addOperationGasCostIsVeryLowTier() { + final MessageFrame frame = + new TestMessageFrameBuilderV2() + .pushStackItem(Bytes32.fromHexString("0x01")) + .pushStackItem(Bytes32.fromHexString("0x02")) + .build(); + + final Operation.OperationResult result = operation.execute(frame, null); + + assertThat(result.getGasCost()).isEqualTo(gasCalculator.getVeryLowTierGasCost()); + } +}