Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion evm/src/main/java/org/hyperledger/besu/evm/EVM.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions evm/src/main/java/org/hyperledger/besu/evm/UInt256.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,7 +29,6 @@
*/
public class AddOperationV2 extends AbstractFixedCostOperationV2 {

@SuppressWarnings("UnusedVariable")
private static final Operation.OperationResult ADD_SUCCESS =
new Operation.OperationResult(3, null);

Expand All @@ -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.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Push order when building the frame: b first (deepest), then a (top).
*/
static Iterable<Arguments> 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what are you asserting here? test code?

Copy link
Copy Markdown
Contributor Author

@siladu siladu Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack remains unmodified by error codepath


final Operation.OperationResult result = AddOperationV2.staticOperation(frame);

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS);
assertThat(frame.stackTopV2()).isEqualTo(1);
Copy link
Copy Markdown
Contributor

@lu-pinto lu-pinto Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: on a halt condition you don't care about the state of the stack since your are halting the frame so this assertion is pointless

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, I think it's reasonable to confirm the operation hasn't modified the stack during its error path, considering it modifies in the happy path.

The fact it isn't used is business logic outside the scope of this test IMO.

}

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