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 @@ -14,7 +14,6 @@
*/
package org.hyperledger.besu.ethereum.vm.operations.v2;

import org.hyperledger.besu.ethereum.vm.operations.BenchmarkHelper;
import org.hyperledger.besu.evm.UInt256;

import java.math.BigInteger;
Expand Down Expand Up @@ -56,7 +55,7 @@ private static int parseSizeBytes(final String s) {
@Setup(Level.Iteration)
@Override
public void setUp() {
frame = BenchmarkHelper.createMessageCallFrame();
frame = BenchmarkHelperV2.createMessageCallFrame();

Case scenario = Case.fromString(opCode(), caseName());
aPool = new UInt256[SAMPLE_SIZE];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.MulOperationV2;

import org.openjdk.jmh.annotations.Param;

public class MulOperationBenchmarkV2 extends BinaryArithmeticOperationBenchmarkV2 {
@Param("MUL_RANDOM_RANDOM")
private String caseName;

@Override
protected Operation.OperationResult invoke(final MessageFrame frame) {
return MulOperationV2.staticOperation(frame);
}

@Override
protected String caseName() {
return caseName;
}

@Override
protected String opCode() {
return "MUL";
}
}
2 changes: 2 additions & 0 deletions evm/src/main/java/org/hyperledger/besu/evm/EVM.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
import org.hyperledger.besu.evm.tracing.OperationTracer;
import org.hyperledger.besu.evm.v2.operation.AddOperationV2;
import org.hyperledger.besu.evm.v2.operation.MulModOperationV2;
import org.hyperledger.besu.evm.v2.operation.MulOperationV2;
import org.hyperledger.besu.evm.v2.operation.SarOperationV2;
import org.hyperledger.besu.evm.v2.operation.ShlOperationV2;
import org.hyperledger.besu.evm.v2.operation.ShrOperationV2;
Expand Down Expand Up @@ -489,6 +490,7 @@ private void runToHaltV2(final MessageFrame frame, final OperationTracer tracing
result =
switch (opcode) {
case 0x01 -> AddOperationV2.staticOperation(frame);
case 0x02 -> MulOperationV2.staticOperation(frame);
case 0x09 -> MulModOperationV2.staticOperation(frame);
case 0x1b ->
enableConstantinople
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.UInt256;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.gascalculator.GasCalculator;

/**
* EVM v2 MUL operation using long[] stack representation.
*
* <p>Each 256-bit word is stored as four longs: index 0 = most significant 64 bits, index 3 = least
* significant 64 bits.
*/
public class MulOperationV2 extends AbstractFixedCostOperationV2 {

private static final OperationResult MUL_SUCCESS = new OperationResult(5, null);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The operation is constructed with a GasCalculator and AbstractFixedCostOperationV2 is given gasCalculator.getLowTierGasCost(), but executeFixedCostOperation() returns a static OperationResult hardcoding gas cost 5. This makes execute(...) ignore the provided gas calculator (e.g., custom calculators or future changes). Prefer deriving the OperationResult gas cost from the calculator (e.g., store a per-instance OperationResult built from getLowTierGasCost()), or adjust the API so the static path can receive the intended gas cost.

Copilot uses AI. Check for mistakes.

/**
* Instantiates a new Mul operation.
*
* @param gasCalculator the gas calculator
*/
public MulOperationV2(final GasCalculator gasCalculator) {
super(0x02, "MUL", 2, 1, gasCalculator, gasCalculator.getLowTierGasCost());
}

@Override
public OperationResult executeFixedCostOperation(final MessageFrame frame, final EVM evm) {
return staticOperation(frame);
}

/**
* Execute the MUL opcode on the v2 long[] stack. MUL: stack[top-2] = stack[top-1] * stack[top-2],
* return top-1
*
* @param frame the message frame
* @return the operation result
*/
public static OperationResult staticOperation(final MessageFrame frame) {
if (!frame.stackHasItemsV2(2)) return UNDERFLOW_RESPONSE;
long[] stack = frame.stackDataV2();
int top = frame.stackTopV2();
final int aOffset = (top - 1) << 2;
final int bOffset = (top - 2) << 2;

UInt256 valueA =
new UInt256(stack[aOffset], stack[aOffset + 1], stack[aOffset + 2], stack[aOffset + 3]);
UInt256 valueB =
new UInt256(stack[bOffset], stack[bOffset + 1], stack[bOffset + 2], stack[bOffset + 3]);

UInt256 r = valueA.mul(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 MUL_SUCCESS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* 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 MulOperationV2Test {
private final GasCalculator gasCalculator = new FrontierGasCalculator();
private final MulOperationV2 operation = new MulOperationV2(gasCalculator);

/**
* Structural test data for MUL(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", "0x06"),
// Zero absorbing element: 0 × b = 0, verifies MUL yields zero when a multiplicand is zero.
Arguments.of("0x00", "0x03", "0x00"),
// 256-bit wrap: 2^255 × 2 = 2^256 ≡ 0 (mod 2^256). All 4 result limbs must be written as
// zero — catches bugs where high limbs are left stale after an overflowing product.
Arguments.of(
"0x8000000000000000000000000000000000000000000000000000000000000000", "0x02", "0x00"),
// All 4 limbs populated on both inputs and the result: verifies every limb is read and
// written through stackDataV2(). Four distinct limb values on `a` catch any
// limb-index/offset mistakes in either input or the output.
Arguments.of(
"0x1000000000000001200000000000000230000000000000034000000000000004",
"0x1000000000000001100000000000000110000000000000011000000000000001",
"0x490000000000000b2700000000000009e4000000000000078000000000000004"));
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.

can you add these two cases :

  • x * 1 = x
  • x * y = y * x

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.

Already covered by property_mul_identity and property_mul_commutative

}

@ParameterizedTest(name = "{index}: MUL({0}, {1}) = {2}")
@MethodSource("data")
void mulOperation(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();
// MUL 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 shouldUnderflowNoItemsEvenOOG() {
final MessageFrame frame = new TestMessageFrameBuilderV2().initialGas(1L).build();
assertThat(frame.stackTopV2()).isEqualTo(0);

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

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS);
assertThat(frame.stackTopV2()).isEqualTo(0);
}

@Test
void shouldUnderflowOnlyOneItem() {
final MessageFrame frame =
new TestMessageFrameBuilderV2()
.pushStackItem(Bytes32.fromHexString("0x01")) // top-2, missing top-1
.build();
assertThat(frame.stackTopV2()).isEqualTo(1);

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

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS);
assertThat(frame.stackTopV2()).isEqualTo(1);
}

@Test
void shouldPreservesDeeperStackItems() {
// 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 MUL)
.pushStackItem(Bytes32.fromHexString("0x02")) // top-2 (b)
.pushStackItem(Bytes32.fromHexString("0x04")) // 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(8)); // 4 * 2
assertThat(getV2StackItem(frame, 1)).isEqualTo(UInt256.fromBytesBE(untouched.toArrayUnsafe()));
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.

👍

}

@Test
void mulOperationGasCostIsLowTier() {
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.getLowTierGasCost());
}
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.

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.

Done 9723474


@Test
void shouldHaltOnInsufficientGas() {
final MessageFrame frame =
new TestMessageFrameBuilderV2()
.pushStackItem(Bytes32.ZERO)
.pushStackItem(Bytes32.ZERO)
.initialGas(1L)
.build();
assertThat(frame.stackTopV2()).isEqualTo(2);

final Operation.OperationResult result = operation.execute(frame, null);

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_GAS);
assertThat(frame.stackTopV2()).isEqualTo(2);
}
}
Loading