systemContracts,
+ @NonNull final HederaOpsDuration hederaOpsDuration) {
+ super(evm, precompiles);
+ this.featureFlags = Objects.requireNonNull(featureFlags);
+ this.precompiles = Objects.requireNonNull(precompiles);
+ this.addressChecks = Objects.requireNonNull(addressChecks);
+ this.systemContracts = Objects.requireNonNull(systemContracts);
+ this.hederaOpsDuration = Objects.requireNonNull(hederaOpsDuration);
+ }
+
+ /**
+ * Starts the execution of a message call based on the contract address of the given frame,
+ * or halts the frame with an appropriate reason if this cannot be done.
+ *
+ * This contract address may reference,
+ *
+ * - A Hedera system contract.
+ * - A native EVM precompile.
+ * - A Hedera system account (up to {@code 0.0.750}).
+ * - A valid lazy-creation target address.
+ * - An existing contract.
+ * - An existing account.
+ *
+ *
+ * @param frame the frame to start
+ * @param tracer the operation tracer
+ */
+ @Override
+ public void start(@NonNull final MessageFrame frame, @NonNull final OperationTracer tracer) {
+ final var codeAddress = frame.getContractAddress();
+ // This must be done first as the system contract address range overlaps with system
+ // accounts. Note that unlike EVM precompiles, we do allow sending value "to" Hedera
+ // system contracts because they sometimes require fees greater than be reasonably
+ // paid using gas; for example, when creating a new token. But the system contract
+ // only diverts this value to the network's fee collection accounts, instead of
+ // actually receiving it.
+ // We do not allow sending value to Hedera system contracts except in the case of token creation.
+ if (systemContracts.containsKey(codeAddress)) {
+ if (!isTokenCreation(frame)) {
+ doHaltIfInvalidSystemCall(frame, tracer);
+ if (alreadyHalted(frame)) {
+ return;
+ }
+ }
+ doExecuteSystemContract(systemContracts.get(codeAddress), codeAddress, frame, tracer);
+ return;
+ }
+
+ var evmPrecompile = precompiles.get(codeAddress);
+ if (evmPrecompile != null && !isPrecompileEnabled(codeAddress, frame)) {
+ // disable precompile if so configured.
+ evmPrecompile = null;
+ }
+
+ // Check to see if the code address is a system account and possibly halt
+ if (addressChecks.isSystemAccount(codeAddress)) {
+ doHaltIfInvalidSystemCall(frame, tracer);
+ if (alreadyHalted(frame)) {
+ return;
+ }
+
+ if (evmPrecompile == null) {
+ handleNonExtantSystemAccount(frame, tracer);
+
+ return;
+ }
+ }
+
+ // Handle evm precompiles
+ if (evmPrecompile != null) {
+ doExecutePrecompile(evmPrecompile, frame, tracer);
+ return;
+ }
+
+ // Transfer value to the contract if required and possibly halt
+ if (transfersValue(frame)) {
+ doTransferValueOrHalt(frame, tracer);
+ if (alreadyHalted(frame)) {
+ return;
+ }
+ }
+
+ // For mono-service fidelity, we need to consider called contracts
+ // as a special case eligible for staking rewards
+ if (isTopLevelTransaction(frame)) {
+ final var maybeCalledContract = proxyUpdaterFor(frame).get(codeAddress);
+ if (maybeCalledContract instanceof ProxyEvmContract a) {
+ recordBuilderFor(frame).trackExplicitRewardSituation(a.hederaId());
+ }
+ }
+
+ frame.setState(MessageFrame.State.CODE_EXECUTING);
+ }
+
+ /**
+ * Checks if the given message frame is a token creation scenario.
+ *
+ * This method inspects the first four bytes of the input data of the message frame
+ * to determine if it matches any of the known selectors for creating fungible or non-fungible tokens.
+ *
+ * @param frame the message frame to check
+ * @return true if the input data matches any of the known create selectors, false otherwise
+ */
+ private boolean isTokenCreation(MessageFrame frame) {
+ if (frame.getInputData().isEmpty()) {
+ return false;
+ }
+ var selector = frame.getInputData().slice(0, 4).toArray();
+ return createMethodsSet.stream().anyMatch(s -> Arrays.equals(s.selector(), selector));
+ }
+
+ /**
+ * @return whether the implicit creation is currently enabled
+ */
+ public boolean isImplicitCreationEnabled() {
+ return featureFlags.isImplicitCreationEnabled();
+ }
+
+ private void handleNonExtantSystemAccount(
+ @NonNull final MessageFrame frame, @NonNull final OperationTracer tracer) {
+ final PrecompileContractResult result = PrecompileContractResult.success(Bytes.EMPTY);
+ frame.clearGasRemaining();
+ finishPrecompileExecution(frame, result, PRECOMPILE, (ActionSidecarContentTracer) tracer);
+ }
+
+ private void doExecutePrecompile(
+ @NonNull final PrecompiledContract precompile,
+ @NonNull final MessageFrame frame,
+ @NonNull final OperationTracer tracer) {
+ final var gasRequirement = precompile.gasRequirement(frame.getInputData());
+ final PrecompileContractResult result;
+ if (frame.getRemainingGas() < gasRequirement) {
+ result = PrecompileContractResult.halt(Bytes.EMPTY, Optional.of(INSUFFICIENT_GAS));
+ } else {
+ frame.decrementRemainingGas(gasRequirement);
+ incrementOpsDuration(
+ frame, gasRequirement * hederaOpsDuration.precompileDurationMultiplier() / MULTIPLIER_FACTOR);
+ result = precompile.computePrecompile(frame.getInputData(), frame);
+ if (result.isRefundGas()) {
+ frame.incrementRemainingGas(gasRequirement);
+ }
+ }
+ // We must always call tracePrecompileResult() to ensure the tracer is in a consistent
+ // state, because AbstractMessageProcessor.process() will not invoke the tracer's
+ // tracePostExecution() method unless start() returns with a state of CODE_EXECUTING;
+ // but for a precompile call this never happens.
+ finishPrecompileExecution(frame, result, PRECOMPILE, (ActionSidecarContentTracer) tracer);
+ }
+
+ /**
+ * This method is necessary as the system contracts do not calculate their gas requirements until after
+ * the call to computePrecompile. Thus, the logic for checking for sufficient gas must be done in a different
+ * order vs normal precompiles.
+ *
+ * @param systemContract the system contract to execute
+ * @param frame the current frame
+ * @param tracer the operation tracer
+ */
+ private void doExecuteSystemContract(
+ @NonNull final HederaSystemContract systemContract,
+ @NonNull final Address systemContractAddress,
+ @NonNull final MessageFrame frame,
+ @NonNull final OperationTracer tracer) {
+ final var fullResult = systemContract.computeFully(
+ ContractID.newBuilder()
+ .contractNum(numberOfLongZero(systemContractAddress))
+ .build(),
+ frame.getInputData(),
+ frame);
+ final var gasRequirement = fullResult.gasRequirement();
+
+ ContractCallContext.get().setGasRequirement(gasRequirement);
+
+ final PrecompileContractResult result;
+ if (frame.getRemainingGas() < gasRequirement) {
+ result = PrecompileContractResult.halt(Bytes.EMPTY, Optional.of(INSUFFICIENT_GAS));
+ } else {
+ if (!fullResult.isRefundGas()) {
+ frame.decrementRemainingGas(gasRequirement);
+ incrementOpsDuration(
+ frame,
+ gasRequirement * hederaOpsDuration.systemContractDurationMultiplier() / MULTIPLIER_FACTOR);
+ }
+ result = fullResult.result();
+ }
+ finishPrecompileExecution(frame, result, SYSTEM, (ActionSidecarContentTracer) tracer);
+ }
+
+ private void finishPrecompileExecution(
+ @NonNull final MessageFrame frame,
+ @NonNull final PrecompileContractResult result,
+ @NonNull final ContractActionType type,
+ @NonNull final ActionSidecarContentTracer tracer) {
+ if (result.getState() == MessageFrame.State.REVERT) {
+ frame.setRevertReason(result.getOutput());
+ } else {
+ frame.setOutputData(result.getOutput());
+ }
+ frame.setState(result.getState());
+ frame.setExceptionalHaltReason(result.getHaltReason());
+ tracer.tracePrecompileCall(frame, ContractCallContext.get().getGasRequirement(), result.getOutput());
+ }
+
+ private void doTransferValueOrHalt(
+ @NonNull final MessageFrame frame, @NonNull final OperationTracer operationTracer) {
+ final var proxyWorldUpdater = (ProxyWorldUpdater) frame.getWorldUpdater();
+ // Try to lazy-create the recipient address if it doesn't exist
+ if (!addressChecks.isPresent(frame.getRecipientAddress(), frame)) {
+ final var maybeReasonToHalt = proxyWorldUpdater.tryLazyCreation(frame.getRecipientAddress(), frame);
+ maybeReasonToHalt.ifPresent(reason -> doHaltOnFailedLazyCreation(frame, reason, operationTracer));
+ }
+ if (!alreadyHalted(frame)) {
+ final var maybeReasonToHalt = proxyWorldUpdater.tryTransfer(
+ frame.getSenderAddress(),
+ frame.getRecipientAddress(),
+ frame.getValue().toLong(),
+ acquiredSenderAuthorizationViaDelegateCall(frame));
+ maybeReasonToHalt.ifPresent(reason -> {
+ if (reason == INVALID_SIGNATURE) {
+ setPropagatedCallFailure(frame, MISSING_RECEIVER_SIGNATURE);
+ }
+ doHalt(frame, reason, operationTracer);
+ });
+ }
+ }
+
+ private void doHaltIfInvalidSystemCall(
+ @NonNull final MessageFrame frame, @NonNull final OperationTracer operationTracer) {
+ if (transfersValue(frame)) {
+ doHalt(frame, INVALID_CONTRACT_ID, operationTracer);
+ }
+ }
+
+ private void doHaltOnFailedLazyCreation(
+ @NonNull final MessageFrame frame,
+ @NonNull final ExceptionalHaltReason reason,
+ @NonNull final OperationTracer tracer) {
+ doHalt(frame, reason, tracer, ForLazyCreation.YES);
+ }
+
+ private void doHalt(
+ @NonNull final MessageFrame frame,
+ @NonNull final ExceptionalHaltReason reason,
+ @NonNull final OperationTracer tracer) {
+ doHalt(frame, reason, tracer, ForLazyCreation.NO);
+ }
+
+ private void doHalt(
+ @NonNull final MessageFrame frame,
+ @NonNull final ExceptionalHaltReason reason,
+ @Nullable final OperationTracer operationTracer,
+ @NonNull final ForLazyCreation forLazyCreation) {
+ frame.setState(EXCEPTIONAL_HALT);
+ frame.setExceptionalHaltReason(Optional.of(reason));
+ if (forLazyCreation == ForLazyCreation.YES) {
+ frame.decrementRemainingGas(frame.getRemainingGas());
+ if (reason == INSUFFICIENT_CHILD_RECORDS) {
+ setPropagatedCallFailure(frame, RESULT_CANNOT_BE_EXTERNALIZED);
+ }
+ }
+ if (operationTracer != null) {
+ if (forLazyCreation == ForLazyCreation.YES) {
+ operationTracer.traceAccountCreationResult(frame, Optional.of(reason));
+ } else {
+ operationTracer.tracePostExecution(
+ frame, new Operation.OperationResult(frame.getRemainingGas(), reason));
+ }
+ }
+ }
+}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/common/ContractCallContext.java b/web3/src/main/java/org/hiero/mirror/web3/common/ContractCallContext.java
index 9dca06983f0..9c3fe6c6e85 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/common/ContractCallContext.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/common/ContractCallContext.java
@@ -16,7 +16,6 @@
import org.hiero.mirror.common.domain.contract.ContractAction;
import org.hiero.mirror.common.domain.transaction.RecordFile;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.Opcode;
-import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracerOptions;
import org.hiero.mirror.web3.evm.store.CachingStateFrame;
import org.hiero.mirror.web3.evm.store.StackedStateFrames;
@@ -42,14 +41,6 @@ public class ContractCallContext {
@Setter
private List contractActions = List.of();
- /**
- * This is used to determine the contract action index of the current frame. It starts from {@code -1} because when
- * the tracer receives the initial frame, it will increment this immediately inside
- * {@link OpcodeTracer#traceContextEnter}.
- */
- @Setter
- private int contractActionIndexOfCurrentFrame = -1;
-
@Setter
private OpcodeTracerOptions opcodeTracerOptions;
@@ -83,6 +74,9 @@ public class ContractCallContext {
@Setter
private boolean isBalanceCall;
+ @Setter
+ private long gasRequirement;
+
private ContractCallContext() {}
public static ContractCallContext get() {
@@ -147,10 +141,6 @@ public boolean useHistorical() {
return recordFile != null; // Remove recordFile comparison after mono code deletion
}
- public void incrementContractActionsCounter() {
- this.contractActionIndexOfCurrentFrame++;
- }
-
/**
* Returns the set timestamp or the consensus end timestamp from the set record file only if we are in a historical
* context. If not - an empty optional is returned.
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java b/web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java
index eba2dbc354d..d44f891df08 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java
@@ -6,6 +6,7 @@
import com.github.benmanes.caffeine.cache.Caffeine;
import com.hedera.hapi.node.base.SemanticVersion;
+import com.hedera.node.app.service.contract.impl.exec.ActionSidecarContentTracer;
import com.hedera.node.app.service.contract.impl.exec.operations.HederaCustomCallOperation;
import com.hedera.node.app.service.evm.contracts.execution.traceability.HederaEvmOperationTracer;
import com.hedera.node.app.service.evm.contracts.operations.CreateOperationExternalizer;
@@ -41,7 +42,9 @@
import org.hiero.mirror.web3.evm.contracts.execution.MirrorEvmMessageCallProcessor;
import org.hiero.mirror.web3.evm.contracts.execution.MirrorEvmMessageCallProcessorV30;
import org.hiero.mirror.web3.evm.contracts.execution.MirrorEvmMessageCallProcessorV50;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationTracer;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerType;
import org.hiero.mirror.web3.evm.contracts.operations.HederaBlockHashOperation;
@@ -63,6 +66,7 @@
import org.hyperledger.besu.evm.processor.ContractCreationProcessor;
import org.hyperledger.besu.evm.processor.MessageCallProcessor;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
@@ -248,7 +252,11 @@ CacheManager cacheManagerRecordFileEarliest() {
}
@Bean
- Map> tracerProvider(
+ @ConditionalOnProperty(
+ name = "hiero.mirror.web3.evm.modularizedServices",
+ havingValue = "false",
+ matchIfMissing = true)
+ Map> monoTracerProvider(
final MirrorOperationTracer mirrorOperationTracer, final OpcodeTracer opcodeTracer) {
Map> tracerMap = new EnumMap<>(TracerType.class);
tracerMap.put(TracerType.OPCODE, () -> opcodeTracer);
@@ -256,6 +264,17 @@ Map> tracerProvider(
return tracerMap;
}
+ @Bean
+ @ConditionalOnProperty(name = "hiero.mirror.web3.evm.modularizedServices", havingValue = "true")
+ Map> tracerProvider(
+ final MirrorOperationActionTracer mirrorOperationActionTracer,
+ final OpcodeActionTracer opcodeActionTracer) {
+ Map> tracerMap = new EnumMap<>(TracerType.class);
+ tracerMap.put(TracerType.OPCODE, () -> opcodeActionTracer);
+ tracerMap.put(TracerType.OPERATION, () -> mirrorOperationActionTracer);
+ return tracerMap;
+ }
+
@Bean
Map> contractCreationProcessorProvider(
final ContractCreationProcessor contractCreationProcessor30,
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java
new file mode 100644
index 00000000000..66ce112de21
--- /dev/null
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package org.hiero.mirror.web3.evm.contracts.execution.traceability;
+
+import com.hedera.hapi.streams.ContractActionType;
+import com.hedera.hapi.streams.ContractActions;
+import com.hedera.node.app.service.contract.impl.exec.ActionSidecarContentTracer;
+import com.hedera.services.utils.EntityIdUtils;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import jakarta.inject.Named;
+import java.util.Optional;
+import lombok.CustomLog;
+import org.apache.commons.lang3.StringUtils;
+import org.hiero.mirror.common.domain.entity.Entity;
+import org.hiero.mirror.web3.evm.properties.TraceProperties;
+import org.hiero.mirror.web3.state.CommonEntityAccessor;
+import org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.operation.Operation;
+
+@Named
+@CustomLog
+public class MirrorOperationActionTracer implements ActionSidecarContentTracer {
+
+ private final TraceProperties traceProperties;
+ private final CommonEntityAccessor commonEntityAccessor;
+
+ public MirrorOperationActionTracer(
+ @NonNull final TraceProperties traceProperties, @NonNull final CommonEntityAccessor commonEntityAccessor) {
+ this.traceProperties = traceProperties;
+ this.commonEntityAccessor = commonEntityAccessor;
+ }
+
+ @Override
+ public void tracePostExecution(
+ @NonNull final MessageFrame frame, @NonNull final Operation.OperationResult operationResult) {
+ if (!traceProperties.isEnabled()) {
+ return;
+ }
+
+ if (traceProperties.stateFilterCheck(frame.getState())) {
+ return;
+ }
+
+ final var recipientAddress = frame.getRecipientAddress();
+ final var recipientNum = recipientAddress != null
+ ? commonEntityAccessor.get(
+ com.hedera.pbj.runtime.io.buffer.Bytes.wrap(recipientAddress.toArray()), Optional.empty())
+ : Optional.empty();
+
+ if (recipientNum.isPresent()
+ && traceProperties.contractFilterCheck(
+ EntityIdUtils.asHexedEvmAddress(((Entity) recipientNum.get()).getId()))) {
+ return;
+ }
+
+ log.info(
+ "type={} operation={}, callDepth={}, contract={}, sender={}, recipient={}, remainingGas={}, revertReason={}, input={}, output={}, return={}",
+ frame.getType(),
+ frame.getCurrentOperation() != null
+ ? frame.getCurrentOperation().getName()
+ : StringUtils.EMPTY,
+ frame.getDepth(),
+ frame.getContractAddress().toShortHexString(),
+ frame.getSenderAddress().toShortHexString(),
+ frame.getRecipientAddress().toShortHexString(),
+ frame.getRemainingGas(),
+ frame.getRevertReason()
+ .orElse(org.apache.tuweni.bytes.Bytes.EMPTY)
+ .toHexString(),
+ frame.getInputData().toShortHexString(),
+ frame.getOutputData().toShortHexString(),
+ frame.getReturnData().toShortHexString());
+ }
+
+ @Override
+ public void traceOriginAction(@NonNull MessageFrame frame) {
+ // NO-OP
+ }
+
+ @Override
+ public void sanitizeTracedActions(@NonNull MessageFrame frame) {
+ // NO-OP
+ }
+
+ @Override
+ public void tracePrecompileResult(@NonNull MessageFrame frame, @NonNull ContractActionType type) {
+ // NO-OP
+ }
+
+ @Override
+ public ContractActions contractActions() {
+ return null;
+ }
+}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracer.java b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracer.java
index d801f73e447..18c5c0ef6b9 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracer.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracer.java
@@ -6,6 +6,7 @@
import jakarta.inject.Named;
import lombok.CustomLog;
import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
import org.apache.tuweni.bytes.Bytes;
import org.hiero.mirror.web3.evm.account.MirrorEvmContractAliases;
import org.hiero.mirror.web3.evm.properties.TraceProperties;
@@ -39,7 +40,9 @@ public void tracePostExecution(final MessageFrame currentFrame, final Operation.
log.info(
"type={} operation={}, callDepth={}, contract={}, sender={}, recipient={}, remainingGas={}, revertReason={}, input={}, output={}, return={}",
currentFrame.getType(),
- currentFrame.getCurrentOperation().getName(),
+ currentFrame.getCurrentOperation() != null
+ ? currentFrame.getCurrentOperation().getName()
+ : StringUtils.EMPTY,
currentFrame.getDepth(),
currentFrame.getContractAddress().toShortHexString(),
currentFrame.getSenderAddress().toShortHexString(),
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracer.java b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracer.java
new file mode 100644
index 00000000000..9300572729f
--- /dev/null
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracer.java
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package org.hiero.mirror.web3.evm.contracts.execution.traceability;
+
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.captureMemory;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.captureStack;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.getRevertReasonFromContractActions;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.isCallToHederaPrecompile;
+
+import com.hedera.hapi.streams.ContractActionType;
+import com.hedera.hapi.streams.ContractActions;
+import com.hedera.node.app.service.contract.impl.exec.ActionSidecarContentTracer;
+import com.hedera.node.app.service.contract.impl.state.ProxyWorldUpdater;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import jakarta.inject.Named;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import lombok.CustomLog;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.tuweni.bytes.Bytes;
+import org.hiero.mirror.web3.common.ContractCallContext;
+import org.hiero.mirror.web3.evm.config.PrecompiledContractProvider;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.evm.ModificationNotAllowedException;
+import org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.operation.Operation.OperationResult;
+import org.hyperledger.besu.evm.precompile.PrecompiledContract;
+
+@Named
+@CustomLog
+public class OpcodeActionTracer implements ActionSidecarContentTracer {
+
+ private final Map hederaPrecompiles;
+
+ public OpcodeActionTracer(@NonNull final PrecompiledContractProvider precompiledContractProvider) {
+ this.hederaPrecompiles = precompiledContractProvider.getHederaPrecompiles().entrySet().stream()
+ .collect(Collectors.toMap(e -> Address.fromHexString(e.getKey()), Map.Entry::getValue));
+ }
+
+ @Override
+ public void tracePostExecution(@NonNull final MessageFrame frame, @NonNull final OperationResult operationResult) {
+ final var context = ContractCallContext.get();
+
+ final var options = context.getOpcodeTracerOptions();
+ final var memory = captureMemory(frame, options);
+ final var stack = captureStack(frame, options);
+ final var storage = captureStorage(frame, options);
+ final var opcode = Opcode.builder()
+ .pc(frame.getPC())
+ .op(frame.getCurrentOperation().getName())
+ .gas(frame.getRemainingGas())
+ .gasCost(operationResult.getGasCost())
+ .depth(frame.getDepth())
+ .stack(stack)
+ .memory(memory)
+ .storage(storage)
+ .reason(frame.getRevertReason().map(Bytes::toString).orElse(null))
+ .build();
+
+ context.addOpcodes(opcode);
+ }
+
+ @Override
+ public void tracePrecompileCall(
+ @NonNull final MessageFrame frame, final long gasRequirement, @Nullable final Bytes output) {
+ final var context = ContractCallContext.get();
+ final var revertReason = isCallToHederaPrecompile(frame, hederaPrecompiles)
+ ? getRevertReasonFromContractActions(context)
+ : frame.getRevertReason();
+
+ final var opcode = Opcode.builder()
+ .pc(frame.getPC())
+ .op(
+ frame.getCurrentOperation() != null
+ ? frame.getCurrentOperation().getName()
+ : StringUtils.EMPTY)
+ .gas(frame.getRemainingGas())
+ .gasCost(output != null && !output.isEmpty() ? gasRequirement : 0L)
+ .depth(frame.getDepth())
+ .stack(Collections.emptyList())
+ .memory(Collections.emptyList())
+ .storage(Collections.emptyMap())
+ .reason(revertReason.map(Bytes::toHexString).orElse(null))
+ .build();
+ context.addOpcodes(opcode);
+ }
+
+ private Map captureStorage(final MessageFrame frame, final OpcodeTracerOptions options) {
+ if (!options.isStorage()) {
+ return Collections.emptyMap();
+ }
+
+ try {
+ final var updates = ((ProxyWorldUpdater) frame.getWorldUpdater()).pendingStorageUpdates();
+ return updates.stream()
+ .flatMap(storageAccesses ->
+ storageAccesses.accesses().stream()) // Properly flatten the nested structure
+ .collect(Collectors.toMap(
+ e -> Bytes.wrap(e.key().toArray()),
+ e -> Bytes.wrap(e.value().toArray()),
+ (v1, v2) -> v1, // in case of duplicates, keep the first value
+ TreeMap::new));
+
+ } catch (final ModificationNotAllowedException e) {
+ log.warn("Failed to retrieve storage contents", e);
+ return Collections.emptyMap();
+ }
+ }
+
+ @Override
+ public void traceOriginAction(@NonNull MessageFrame frame) {
+ // NO-OP
+ }
+
+ @Override
+ public void sanitizeTracedActions(@NonNull MessageFrame frame) {
+ // NO-OP
+ }
+
+ @Override
+ public void tracePrecompileResult(@NonNull MessageFrame frame, @NonNull ContractActionType type) {
+ // NO-OP
+ }
+
+ @Override
+ public ContractActions contractActions() {
+ return null;
+ }
+}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracer.java b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracer.java
index 61bfe78b0d8..2a7cfef21fa 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracer.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracer.java
@@ -2,23 +2,14 @@
package org.hiero.mirror.web3.evm.contracts.execution.traceability;
-import static com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason.INVALID_SOLIDITY_ADDRESS;
-import static com.hedera.services.stream.proto.ContractActionType.PRECOMPILE;
-import static org.hyperledger.besu.evm.frame.MessageFrame.State.CODE_SUSPENDED;
-import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_FAILED;
-import static org.hyperledger.besu.evm.frame.MessageFrame.State.EXCEPTIONAL_HALT;
-import static org.hyperledger.besu.evm.frame.MessageFrame.Type.MESSAGE_CALL;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.captureMemory;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.captureStack;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.getRevertReasonFromContractActions;
+import static org.hiero.mirror.web3.evm.contracts.execution.traceability.TracerUtils.isCallToHederaPrecompile;
-import com.hedera.hapi.node.state.contract.SlotKey;
-import com.hedera.hapi.node.state.contract.SlotValue;
-import com.hedera.node.app.service.contract.ContractService;
-import com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason;
import com.hedera.node.app.service.mono.contracts.execution.traceability.HederaOperationTracer;
import com.hedera.services.stream.proto.ContractActionType;
-import com.hedera.services.utils.EntityIdUtils;
-import com.hederahashgraph.api.proto.java.ResponseCodeEnum;
import jakarta.inject.Named;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -29,22 +20,14 @@
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.tuweni.bytes.Bytes;
-import org.hiero.mirror.common.domain.contract.ContractAction;
import org.hiero.mirror.web3.common.ContractCallContext;
-import org.hiero.mirror.web3.convert.BytesDecoder;
import org.hiero.mirror.web3.evm.config.PrecompiledContractProvider;
-import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
-import org.hiero.mirror.web3.state.MirrorNodeState;
-import org.hiero.mirror.web3.state.core.MapWritableStates;
-import org.hiero.mirror.web3.state.keyvalue.ContractStorageReadableKVState;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.evm.ModificationNotAllowedException;
import org.hyperledger.besu.evm.account.MutableAccount;
-import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.operation.Operation;
import org.hyperledger.besu.evm.precompile.PrecompiledContract;
-import org.springframework.util.CollectionUtils;
@Named
@CustomLog
@@ -52,35 +35,21 @@
public class OpcodeTracer implements HederaOperationTracer {
private final Map hederaPrecompiles;
- private final MirrorNodeEvmProperties evmProperties;
- private final MirrorNodeState mirrorNodeState;
- public OpcodeTracer(
- final PrecompiledContractProvider precompiledContractProvider,
- final MirrorNodeEvmProperties evmProperties,
- final MirrorNodeState mirrorNodeState) {
+ public OpcodeTracer(final PrecompiledContractProvider precompiledContractProvider) {
this.hederaPrecompiles = precompiledContractProvider.getHederaPrecompiles().entrySet().stream()
.collect(Collectors.toMap(e -> Address.fromHexString(e.getKey()), Map.Entry::getValue));
- this.evmProperties = evmProperties;
- this.mirrorNodeState = mirrorNodeState;
- }
-
- @Override
- public void init(final MessageFrame frame) {
- getContext().incrementContractActionsCounter();
}
@Override
public void tracePostExecution(final MessageFrame frame, final Operation.OperationResult operationResult) {
- ContractCallContext context = getContext();
- if (frame.getState() == CODE_SUSPENDED) {
- context.incrementContractActionsCounter();
- }
- OpcodeTracerOptions options = context.getOpcodeTracerOptions();
+ final ContractCallContext context = ContractCallContext.get();
+
+ final OpcodeTracerOptions options = context.getOpcodeTracerOptions();
final List memory = captureMemory(frame, options);
final List stack = captureStack(frame, options);
final Map storage = captureStorage(frame, options);
- Opcode opcode = Opcode.builder()
+ final Opcode opcode = Opcode.builder()
.pc(frame.getPC())
.op(frame.getCurrentOperation().getName())
.gas(frame.getRemainingGas())
@@ -97,10 +66,11 @@ public void tracePostExecution(final MessageFrame frame, final Operation.Operati
@Override
public void tracePrecompileCall(final MessageFrame frame, final long gasRequirement, final Bytes output) {
- ContractCallContext context = getContext();
- Optional revertReason =
- isCallToHederaPrecompile(frame) ? getRevertReasonFromContractActions(context) : frame.getRevertReason();
- Opcode opcode = Opcode.builder()
+ final ContractCallContext context = ContractCallContext.get();
+ final Optional revertReason = isCallToHederaPrecompile(frame, hederaPrecompiles)
+ ? getRevertReasonFromContractActions(context)
+ : frame.getRevertReason();
+ final Opcode opcode = Opcode.builder()
.pc(frame.getPC())
.op(
frame.getCurrentOperation() != null
@@ -118,52 +88,6 @@ public void tracePrecompileCall(final MessageFrame frame, final long gasRequirem
context.addOpcodes(opcode);
}
- @Override
- public void traceAccountCreationResult(MessageFrame frame, Optional haltReason) {
- if (haltReason.isPresent() && existsSyntheticActionForFrame(frame)) {
- getContext().incrementContractActionsCounter();
- }
- }
-
- @Override
- public void tracePrecompileResult(MessageFrame frame, ContractActionType type) {
- if (type.equals(PRECOMPILE) && frame.getState().equals(EXCEPTIONAL_HALT)) {
- // if an ETH precompile call exceptional halted, the action is already finalized
- return;
- }
- if (existsSyntheticActionForFrame(frame)) {
- getContext().incrementContractActionsCounter();
- }
- }
-
- private List captureMemory(final MessageFrame frame, OpcodeTracerOptions options) {
- if (!options.isMemory()) {
- return Collections.emptyList();
- }
-
- int size = frame.memoryWordSize();
- var memory = new ArrayList(size);
- for (int i = 0; i < size; i++) {
- memory.add(frame.readMemory(i * 32L, 32));
- }
-
- return memory;
- }
-
- private List captureStack(final MessageFrame frame, OpcodeTracerOptions options) {
- if (!options.isStack()) {
- return Collections.emptyList();
- }
-
- int size = frame.stackSize();
- var stack = new ArrayList(size);
- for (int i = 0; i < size; ++i) {
- stack.add(frame.getStackItem(size - 1 - i));
- }
-
- return stack;
- }
-
private Map captureStorage(final MessageFrame frame, OpcodeTracerOptions options) {
if (!options.isStorage()) {
return Collections.emptyMap();
@@ -178,10 +102,6 @@ private Map captureStorage(final MessageFrame frame, OpcodeTracerO
return Collections.emptyMap();
}
- if (options.modularized) {
- return getModularizedUpdatedStorage(address);
- }
-
return new TreeMap<>(account.getUpdatedStorage());
} catch (final ModificationNotAllowedException e) {
log.warn("Failed to retrieve storage contents", e);
@@ -189,97 +109,8 @@ private Map captureStorage(final MessageFrame frame, OpcodeTracerO
}
}
- private Optional getRevertReasonFromContractActions(ContractCallContext context) {
- List contractActions = context.getContractActions();
-
- if (CollectionUtils.isEmpty(contractActions)) {
- return Optional.empty();
- }
-
- int currentActionIndex = context.getContractActionIndexOfCurrentFrame();
-
- return contractActions.stream()
- .filter(action -> action.hasRevertReason() && action.getIndex() == currentActionIndex)
- .map(action -> Bytes.of(action.getResultData()))
- .map(this::formatRevertReason)
- .findFirst();
- }
-
- public ContractCallContext getContext() {
- return ContractCallContext.get();
- }
-
- private boolean isCallToHederaPrecompile(MessageFrame frame) {
- Address recipientAddress = frame.getRecipientAddress();
- return hederaPrecompiles.containsKey(recipientAddress);
- }
-
- /**
- * When a contract tries to call a non-existing address (resulting in a
- * {@link HederaExceptionalHaltReason#INVALID_SOLIDITY_ADDRESS} failure), a synthetic action is created to record
- * this, otherwise the details of the intended call (e.g. the targeted invalid address) and sequence of events
- * leading to the failure are lost
- */
- private boolean existsSyntheticActionForFrame(MessageFrame frame) {
- return (frame.getState() == EXCEPTIONAL_HALT || frame.getState() == COMPLETED_FAILED)
- && frame.getType().equals(MESSAGE_CALL)
- && frame.getExceptionalHaltReason().isPresent()
- && frame.getExceptionalHaltReason().get().equals(INVALID_SOLIDITY_ADDRESS);
- }
-
- /**
- * Formats the revert reason to be consistent with the revert reason format in the EVM. ...
- *
- * @param revertReason the revert reason
- * @return the formatted revert reason
- */
- private Bytes formatRevertReason(final Bytes revertReason) {
- if (revertReason == null || revertReason.isZero()) {
- return Bytes.EMPTY;
- }
-
- // covers an edge case where the reason in the contract actions is a response code number (as a plain string)
- // so we convert this number to an ABI-encoded string of the corresponding response code name,
- // to at least give some relevant information to the user in the valid EVM format
- Bytes trimmedReason = revertReason.trimLeadingZeros();
- if (trimmedReason.size() <= Integer.BYTES) {
- ResponseCodeEnum responseCode = ResponseCodeEnum.forNumber(trimmedReason.toInt());
- if (responseCode != null) {
- return BytesDecoder.getAbiEncodedRevertReason(responseCode.name());
- }
- }
-
- return BytesDecoder.getAbiEncodedRevertReason(revertReason);
- }
-
- private Map getModularizedUpdatedStorage(Address accountAddress) {
- Map storageUpdates = new TreeMap<>();
- MapWritableStates states = (MapWritableStates) mirrorNodeState.getWritableStates(ContractService.NAME);
-
- try {
- var accountContractID = EntityIdUtils.toContractID(accountAddress);
- var storageState = states.get(ContractStorageReadableKVState.KEY);
- storageState.modifiedKeys().stream()
- .filter(SlotKey.class::isInstance)
- .map(SlotKey.class::cast)
- .filter(slotKey -> accountContractID.equals(slotKey.contractID()))
- .forEach(slotKey -> {
- SlotValue slotValue = (SlotValue) storageState.get(slotKey);
- if (slotValue != null) {
- storageUpdates.put(
- Bytes.wrap(slotKey.key().toByteArray()),
- Bytes.wrap(slotValue.value().toByteArray()));
- }
- });
- } catch (IllegalArgumentException e) {
- log.warn(
- "Failed to retrieve modified storage keys for service: {}, key: {}",
- ContractService.NAME,
- ContractStorageReadableKVState.KEY,
- e);
- }
-
- return storageUpdates;
+ @Override
+ public void tracePrecompileResult(final MessageFrame frame, final ContractActionType type) {
+ // Empty body
}
}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/TracerUtils.java b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/TracerUtils.java
new file mode 100644
index 00000000000..4d770f7bc87
--- /dev/null
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/TracerUtils.java
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package org.hiero.mirror.web3.evm.contracts.execution.traceability;
+
+import com.hederahashgraph.api.proto.java.ResponseCodeEnum;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import lombok.experimental.UtilityClass;
+import org.apache.tuweni.bytes.Bytes;
+import org.hiero.mirror.common.domain.contract.ContractAction;
+import org.hiero.mirror.web3.common.ContractCallContext;
+import org.hiero.mirror.web3.convert.BytesDecoder;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.precompile.PrecompiledContract;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Common utility class with methods used for tracing information
+ */
+@UtilityClass
+public class TracerUtils {
+
+ public static List captureMemory(final MessageFrame frame, OpcodeTracerOptions options) {
+ if (!options.isMemory()) {
+ return Collections.emptyList();
+ }
+
+ int size = frame.memoryWordSize();
+ var memory = new ArrayList(size);
+ for (int i = 0; i < size; i++) {
+ memory.add(frame.readMemory(i * 32L, 32));
+ }
+
+ return memory;
+ }
+
+ public static List captureStack(final MessageFrame frame, OpcodeTracerOptions options) {
+ if (!options.isStack()) {
+ return Collections.emptyList();
+ }
+
+ int size = frame.stackSize();
+ var stack = new ArrayList(size);
+ for (int i = 0; i < size; ++i) {
+ stack.add(frame.getStackItem(size - 1 - i));
+ }
+
+ return stack;
+ }
+
+ public static Optional getRevertReasonFromContractActions(final ContractCallContext context) {
+ List contractActions = context.getContractActions();
+
+ if (CollectionUtils.isEmpty(contractActions)) {
+ return Optional.empty();
+ }
+
+ return contractActions.stream()
+ .filter(ContractAction::hasRevertReason)
+ .map(action -> Bytes.of(action.getResultData()))
+ .map(TracerUtils::formatRevertReason)
+ .findFirst();
+ }
+
+ public static boolean isCallToHederaPrecompile(
+ final MessageFrame frame, final Map hederaPrecompiles) {
+ final var recipientAddress = frame.getRecipientAddress();
+ return hederaPrecompiles.containsKey(recipientAddress);
+ }
+
+ /**
+ * Formats the revert reason to be consistent with the revert reason format in the EVM. ...
+ *
+ * @param revertReason the revert reason
+ * @return the formatted revert reason
+ */
+ public static Bytes formatRevertReason(final Bytes revertReason) {
+ if (revertReason == null || revertReason.isZero()) {
+ return Bytes.EMPTY;
+ }
+
+ // covers an edge case where the reason in the contract actions is a response code number (as a plain string)
+ // so we convert this number to an ABI-encoded string of the corresponding response code name,
+ // to at least give some relevant information to the user in the valid EVM format
+ final var trimmedReason = revertReason.trimLeadingZeros();
+ if (trimmedReason.size() <= Integer.BYTES) {
+ final var responseCode = ResponseCodeEnum.forNumber(trimmedReason.toInt());
+ if (responseCode != null) {
+ return BytesDecoder.getAbiEncodedRevertReason(responseCode.name());
+ }
+ }
+
+ return BytesDecoder.getAbiEncodedRevertReason(revertReason);
+ }
+}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/evm/exception/ResponseCodeUtil.java b/web3/src/main/java/org/hiero/mirror/web3/evm/exception/ResponseCodeUtil.java
index a2413661080..4a7fd5a2a2f 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/evm/exception/ResponseCodeUtil.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/evm/exception/ResponseCodeUtil.java
@@ -3,8 +3,12 @@
package org.hiero.mirror.web3.evm.exception;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_EXECUTION_EXCEPTION;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_NEGATIVE_VALUE;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_BALANCES_FOR_STORAGE_RENT;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CONTRACT_ID;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SOLIDITY_ADDRESS;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_TRANSACTION;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.MAX_CHILD_RECORDS_EXCEEDED;
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.MAX_CONTRACT_STORAGE_EXCEEDED;
@@ -32,7 +36,12 @@ public class ResponseCodeUtil {
MAX_STORAGE_IN_PRICE_REGIME_HAS_BEEN_USED,
MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED,
INSUFFICIENT_BALANCES_FOR_STORAGE_RENT,
- INVALID_TRANSACTION)
+ INVALID_TRANSACTION,
+ CONTRACT_EXECUTION_EXCEPTION,
+ INVALID_SOLIDITY_ADDRESS,
+ INVALID_CONTRACT_ID,
+ CONTRACT_NEGATIVE_VALUE,
+ INSUFFICIENT_PAYER_BALANCE)
.collect(toMap(
status ->
new BytesKey(new MirrorEvmTransactionException(status, StringUtils.EMPTY, StringUtils.EMPTY)
@@ -52,7 +61,7 @@ public static ResponseCodeEnum getStatusOrDefault(final HederaEvmTransactionProc
} else if (ExceptionalHaltReason.INSUFFICIENT_GAS == haltReason) {
return ResponseCodeEnum.INSUFFICIENT_GAS;
} else if (HederaExceptionalHaltReason.INVALID_SOLIDITY_ADDRESS == haltReason) {
- return ResponseCodeEnum.INVALID_SOLIDITY_ADDRESS;
+ return INVALID_SOLIDITY_ADDRESS;
}
}
diff --git a/web3/src/main/java/org/hiero/mirror/web3/service/TransactionExecutionService.java b/web3/src/main/java/org/hiero/mirror/web3/service/TransactionExecutionService.java
index 033e7c22f0a..3111ac78ce4 100644
--- a/web3/src/main/java/org/hiero/mirror/web3/service/TransactionExecutionService.java
+++ b/web3/src/main/java/org/hiero/mirror/web3/service/TransactionExecutionService.java
@@ -36,7 +36,9 @@
import org.hiero.mirror.common.CommonProperties;
import org.hiero.mirror.common.domain.SystemEntity;
import org.hiero.mirror.web3.common.ContractCallContext;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationTracer;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracer;
import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
import org.hiero.mirror.web3.exception.MirrorEvmTransactionException;
@@ -62,6 +64,8 @@ public class TransactionExecutionService {
private final MirrorNodeEvmProperties mirrorNodeEvmProperties;
private final OpcodeTracer opcodeTracer;
private final MirrorOperationTracer mirrorOperationTracer;
+ private final OpcodeActionTracer opcodeActionTracer;
+ private final MirrorOperationActionTracer mirrorOperationActionTracer;
private final SystemEntity systemEntity;
private final TransactionExecutorFactory transactionExecutorFactory;
@@ -259,8 +263,8 @@ private void throwPayerAccountNotFoundException(final String message) {
private OperationTracer[] getOperationTracers() {
return ContractCallContext.get().getOpcodeTracerOptions() != null
- ? new OperationTracer[] {opcodeTracer}
- : new OperationTracer[] {mirrorOperationTracer};
+ ? new OperationTracer[] {opcodeActionTracer}
+ : new OperationTracer[] {mirrorOperationActionTracer};
}
private SequencedCollection populateChildTransactionErrors(
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorBaseTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorBaseTest.java
index 19f3d8eee19..0c33bb6ccb0 100644
--- a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorBaseTest.java
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorBaseTest.java
@@ -115,7 +115,7 @@ public abstract class MirrorEvmMessageCallProcessorBaseTest {
PRNG_PRECOMPILE_ADDRESS, prngSystemPrecompiledContract);
@Spy
- private ContractCallContext contractCallContext;
+ protected ContractCallContext contractCallContext;
@BeforeAll
static void initStaticMocks() {
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorTest.java
index 9d2622bee23..2ff9b15d829 100644
--- a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorTest.java
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/MirrorEvmMessageCallProcessorTest.java
@@ -23,11 +23,11 @@
import java.util.Optional;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.tuweni.bytes.Bytes;
+import org.hiero.mirror.web3.common.ContractCallContext;
import org.hiero.mirror.web3.evm.config.PrecompilesHolder;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracerOptions;
import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
-import org.hiero.mirror.web3.state.MirrorNodeState;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
@@ -37,6 +37,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
class MirrorEvmMessageCallProcessorTest extends MirrorEvmMessageCallProcessorBaseTest {
@@ -44,15 +45,14 @@ class MirrorEvmMessageCallProcessorTest extends MirrorEvmMessageCallProcessorBas
private static final Address NON_PRECOMPILE_ADDRESS =
Address.fromHexString("0x00a94f5374fce5edbc8e2a8697c15331677e6ebf");
+ private static MockedStatic contextMockedStatic;
+
@Mock
private PrecompilesHolder precompilesHolder;
@Mock
private MirrorNodeEvmProperties evmProperties;
- @Mock
- private MirrorNodeState mirrorNodeState;
-
private OpcodeTracer opcodeTracer;
private MirrorEvmMessageCallProcessor subject;
@@ -60,7 +60,7 @@ class MirrorEvmMessageCallProcessorTest extends MirrorEvmMessageCallProcessorBas
void setUp() {
when(precompilesHolder.getHederaPrecompiles()).thenReturn(hederaPrecompileList);
when(messageFrame.getWorldUpdater()).thenReturn(updater);
- opcodeTracer = Mockito.spy(new OpcodeTracer(precompilesHolder, evmProperties, mirrorNodeState));
+ opcodeTracer = Mockito.spy(new OpcodeTracer(precompilesHolder));
subject = new MirrorEvmMessageCallProcessor(
autoCreationLogic,
entityAddressSequencer,
@@ -163,7 +163,8 @@ void startWithNonHTSPrecompiledContractAddress() {
when(messageFrame.getGasPrice()).thenReturn(Wei.ONE);
boolean isModularized = evmProperties.isModularizedServices();
- when(opcodeTracer.getContext().getOpcodeTracerOptions())
+
+ when(contractCallContext.getOpcodeTracerOptions())
.thenReturn(new OpcodeTracerOptions(true, true, true, isModularized));
when(messageFrame.getCurrentOperation()).thenReturn(mock(Operation.class));
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracerTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracerTest.java
new file mode 100644
index 00000000000..02721e86778
--- /dev/null
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracerTest.java
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package org.hiero.mirror.web3.evm.contracts.execution.traceability;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+import java.util.Optional;
+import java.util.Set;
+import org.apache.tuweni.bytes.Bytes;
+import org.hiero.mirror.common.domain.entity.Entity;
+import org.hiero.mirror.web3.evm.properties.TraceProperties;
+import org.hiero.mirror.web3.state.CommonEntityAccessor;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.frame.MessageFrame.State;
+import org.hyperledger.besu.evm.frame.MessageFrame.Type;
+import org.hyperledger.besu.evm.operation.BalanceOperation;
+import org.hyperledger.besu.evm.operation.Operation;
+import org.hyperledger.besu.evm.operation.Operation.OperationResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+
+@ExtendWith({MockitoExtension.class, OutputCaptureExtension.class})
+class MirrorOperationActionTracerTest {
+
+ private static final Address contract = Address.fromHexString("0x2");
+ private static final Address recipient = Address.fromHexString("0x3");
+ private static final Address sender = Address.fromHexString("0x4");
+ private static final long INITIAL_GAS = 1000L;
+ private static final Bytes input = Bytes.of("inputData".getBytes());
+ private static final Operation operation = new BalanceOperation(null);
+ private static final Bytes outputData = Bytes.of("outputData".getBytes());
+ private static final Bytes returnData = Bytes.of("returnData".getBytes());
+
+ @Mock
+ private OperationResult operationResult;
+
+ private TraceProperties traceProperties;
+
+ @Mock
+ private MessageFrame messageFrame;
+
+ @Mock
+ private Entity recipientEntity;
+
+ @Mock
+ private CommonEntityAccessor commonEntityAccessor;
+
+ private MirrorOperationActionTracer mirrorOperationTracer;
+
+ @BeforeEach
+ void setup() {
+ traceProperties = new TraceProperties();
+ mirrorOperationTracer = new MirrorOperationActionTracer(traceProperties, commonEntityAccessor);
+ }
+
+ @Test
+ void traceDisabled(CapturedOutput output) {
+ traceProperties.setEnabled(false);
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+ assertThat(output).doesNotContain("type=");
+ }
+
+ @Test
+ void stateFilterMismatch(CapturedOutput output) {
+ traceProperties.setEnabled(true);
+ traceProperties.setStatus(Set.of(State.CODE_EXECUTING));
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+
+ assertThat(output).doesNotContain("type=");
+ }
+
+ @Test
+ void stateFilter(CapturedOutput output) {
+ traceProperties.setEnabled(true);
+ traceProperties.setStatus(Set.of(State.CODE_SUSPENDED));
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getType()).willReturn(Type.MESSAGE_CALL);
+ given(messageFrame.getContractAddress()).willReturn(contract);
+ given(messageFrame.getCurrentOperation()).willReturn(operation);
+ given(messageFrame.getRemainingGas()).willReturn(INITIAL_GAS);
+ given(messageFrame.getInputData()).willReturn(input);
+ given(messageFrame.getOutputData()).willReturn(outputData);
+ given(messageFrame.getRecipientAddress()).willReturn(recipient);
+ given(messageFrame.getReturnData()).willReturn(returnData);
+ given(messageFrame.getSenderAddress()).willReturn(sender);
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getDepth()).willReturn(1);
+
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+
+ assertThat(output)
+ .contains(
+ "type=MESSAGE_CALL",
+ "callDepth=1",
+ "recipient=0x3",
+ "input=0x696e70757444617461",
+ "operation=BALANCE",
+ "output=0x6f757470757444617461",
+ "remainingGas=1000",
+ "return=0x72657475726e44617461",
+ "revertReason=",
+ "sender=0x4");
+ }
+
+ @Test
+ void contractFilterMismatch(CapturedOutput output) {
+ traceProperties.setEnabled(true);
+ traceProperties.setContract(Set.of(contract.toHexString()));
+ given(messageFrame.getRecipientAddress()).willReturn(recipient);
+ given(commonEntityAccessor.get(
+ com.hedera.pbj.runtime.io.buffer.Bytes.wrap(recipient.toArray()), Optional.empty()))
+ .willReturn(Optional.of(recipientEntity));
+ given(recipientEntity.getId()).willReturn(3L);
+
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+
+ assertThat(output).doesNotContain("type=");
+ }
+
+ @Test
+ void contractFilter(CapturedOutput output) {
+ traceProperties.setEnabled(true);
+ traceProperties.setContract(Set.of(recipient.toHexString()));
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getType()).willReturn(Type.MESSAGE_CALL);
+ given(messageFrame.getContractAddress()).willReturn(contract);
+ given(messageFrame.getCurrentOperation()).willReturn(operation);
+ given(messageFrame.getRemainingGas()).willReturn(INITIAL_GAS);
+ given(messageFrame.getInputData()).willReturn(input);
+ given(messageFrame.getOutputData()).willReturn(outputData);
+ given(messageFrame.getRecipientAddress()).willReturn(recipient);
+ given(messageFrame.getReturnData()).willReturn(returnData);
+ given(messageFrame.getSenderAddress()).willReturn(sender);
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getDepth()).willReturn(1);
+
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+
+ assertThat(output)
+ .contains(
+ "type=MESSAGE_CALL",
+ "callDepth=1",
+ "recipient=0x3",
+ "input=0x696e70757444617461",
+ "operation=BALANCE",
+ "output=0x6f757470757444617461",
+ "remainingGas=1000",
+ "revertReason=",
+ "return=0x72657475726e44617461",
+ "sender=0x4");
+ }
+
+ @Test
+ void tracePostExecution(CapturedOutput output) {
+ traceProperties.setEnabled(true);
+
+ given(messageFrame.getType()).willReturn(Type.MESSAGE_CALL);
+ given(messageFrame.getContractAddress()).willReturn(contract);
+ given(messageFrame.getCurrentOperation()).willReturn(operation);
+ given(messageFrame.getRemainingGas()).willReturn(INITIAL_GAS);
+ given(messageFrame.getInputData()).willReturn(input);
+ given(messageFrame.getOutputData()).willReturn(outputData);
+ given(messageFrame.getRecipientAddress()).willReturn(recipient);
+ given(messageFrame.getReturnData()).willReturn(returnData);
+ given(messageFrame.getSenderAddress()).willReturn(sender);
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+
+ given(messageFrame.getType()).willReturn(Type.MESSAGE_CALL);
+ given(messageFrame.getContractAddress()).willReturn(contract);
+ given(messageFrame.getCurrentOperation()).willReturn(operation);
+ given(messageFrame.getRemainingGas()).willReturn(INITIAL_GAS);
+ given(messageFrame.getInputData()).willReturn(input);
+ given(messageFrame.getOutputData()).willReturn(outputData);
+ given(messageFrame.getRecipientAddress()).willReturn(recipient);
+ given(messageFrame.getReturnData()).willReturn(returnData);
+ given(messageFrame.getSenderAddress()).willReturn(sender);
+ given(messageFrame.getState()).willReturn(State.CODE_SUSPENDED);
+ given(messageFrame.getDepth()).willReturn(1);
+
+ mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
+ assertThat(output)
+ .contains(
+ "type=MESSAGE_CALL",
+ "callDepth=0",
+ "recipient=0x3",
+ "input=0x696e70757444617461",
+ "operation=BALANCE",
+ "output=0x6f757470757444617461",
+ "remainingGas=1000",
+ "revertReason=",
+ "return=0x72657475726e44617461",
+ "sender=0x4")
+ .contains(
+ "type=MESSAGE_CALL",
+ "callDepth=1",
+ "recipient=0x3",
+ "input=0x696e70757444617461",
+ "operation=BALANCE",
+ "output=0x6f757470757444617461",
+ "remainingGas=1000",
+ "return=0x72657475726e44617461",
+ "revertReason=",
+ "sender=0x4");
+ }
+}
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracerTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracerTest.java
index 16a5ad12250..4f72f669577 100644
--- a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracerTest.java
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationTracerTest.java
@@ -60,7 +60,7 @@ void setup() {
void traceDisabled(CapturedOutput output) {
traceProperties.setEnabled(false);
mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
- assertThat(output).isEmpty();
+ assertThat(output).doesNotContain("type=");
}
@Test
@@ -71,7 +71,7 @@ void stateFilterMismatch(CapturedOutput output) {
mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
- assertThat(output).isEmpty();
+ assertThat(output).doesNotContain("type=");
}
@Test
@@ -119,7 +119,7 @@ void contractFilterMismatch(CapturedOutput output) {
mirrorOperationTracer.tracePostExecution(messageFrame, operationResult);
- assertThat(output).isEmpty();
+ assertThat(output).doesNotContain("type=");
}
@Test
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracerTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracerTest.java
new file mode 100644
index 00000000000..8b6aafb059f
--- /dev/null
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeActionTracerTest.java
@@ -0,0 +1,732 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package org.hiero.mirror.web3.evm.contracts.execution.traceability;
+
+import static com.hedera.services.store.contracts.precompile.ExchangeRatePrecompiledContract.EXCHANGE_RATE_SYSTEM_CONTRACT_ADDRESS;
+import static com.hedera.services.store.contracts.precompile.PrngSystemPrecompiledContract.PRNG_PRECOMPILE_ADDRESS;
+import static com.hedera.services.store.contracts.precompile.SyntheticTxnFactory.HTS_PRECOMPILED_CONTRACT_ADDRESS;
+import static com.hedera.services.stream.proto.ContractAction.ResultDataCase.OUTPUT;
+import static com.hedera.services.stream.proto.ContractAction.ResultDataCase.REVERT_REASON;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hiero.mirror.web3.convert.BytesDecoder.getAbiEncodedRevertReason;
+import static org.hiero.mirror.web3.evm.utils.EvmTokenUtils.toAddress;
+import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INSUFFICIENT_GAS;
+import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INVALID_OPERATION;
+import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_FAILED;
+import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_SUCCESS;
+import static org.hyperledger.besu.evm.frame.MessageFrame.State.EXCEPTIONAL_HALT;
+import static org.hyperledger.besu.evm.frame.MessageFrame.State.NOT_STARTED;
+import static org.hyperledger.besu.evm.frame.MessageFrame.Type.CONTRACT_CREATION;
+import static org.hyperledger.besu.evm.frame.MessageFrame.Type.MESSAGE_CALL;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.hedera.hapi.node.base.ContractID;
+import com.hedera.node.app.service.contract.impl.state.ProxyWorldUpdater;
+import com.hedera.node.app.service.contract.impl.state.StorageAccess;
+import com.hedera.node.app.service.contract.impl.state.StorageAccesses;
+import com.hedera.services.stream.proto.CallOperationType;
+import com.hedera.services.stream.proto.ContractActionType;
+import com.hederahashgraph.api.proto.java.ResponseCodeEnum;
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.CustomLog;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.apache.tuweni.units.bigints.UInt256;
+import org.assertj.core.util.Lists;
+import org.hiero.mirror.common.domain.contract.ContractAction;
+import org.hiero.mirror.common.domain.entity.EntityId;
+import org.hiero.mirror.common.domain.entity.EntityType;
+import org.hiero.mirror.web3.common.ContractCallContext;
+import org.hiero.mirror.web3.evm.config.PrecompilesHolder;
+import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
+import org.hyperledger.besu.datatypes.Address;
+import org.hyperledger.besu.datatypes.Hash;
+import org.hyperledger.besu.datatypes.Wei;
+import org.hyperledger.besu.evm.EVM;
+import org.hyperledger.besu.evm.ModificationNotAllowedException;
+import org.hyperledger.besu.evm.account.MutableAccount;
+import org.hyperledger.besu.evm.code.CodeV0;
+import org.hyperledger.besu.evm.frame.BlockValues;
+import org.hyperledger.besu.evm.frame.ExceptionalHaltReason;
+import org.hyperledger.besu.evm.frame.MessageFrame;
+import org.hyperledger.besu.evm.operation.AbstractOperation;
+import org.hyperledger.besu.evm.operation.Operation;
+import org.hyperledger.besu.evm.operation.Operation.OperationResult;
+import org.hyperledger.besu.evm.precompile.PrecompiledContract;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@CustomLog
+@DisplayName("OpcodeActionTracer")
+@ExtendWith(MockitoExtension.class)
+class OpcodeActionTracerTest {
+
+ private static final Address CONTRACT_ADDRESS = Address.fromHexString("0x123");
+ private static final Address ETH_PRECOMPILE_ADDRESS = Address.fromHexString("0x01");
+ private static final Address HTS_PRECOMPILE_ADDRESS = Address.fromHexString(HTS_PRECOMPILED_CONTRACT_ADDRESS);
+ private static final long INITIAL_GAS = 1000L;
+ private static final long GAS_COST = 2L;
+ private static final long GAS_PRICE = 200L;
+ private static final long GAS_REQUIREMENT = 100L;
+ private static final Bytes DEFAULT_OUTPUT = Bytes.fromHexString("0x1234567890abcdef");
+ private static final AtomicReference REMAINING_GAS = new AtomicReference<>();
+ private static final AtomicReference EXECUTED_FRAMES = new AtomicReference<>(0);
+ private static final Operation OPERATION = new AbstractOperation(0x02, "MUL", 2, 1, null) {
+ @Override
+ public OperationResult execute(final MessageFrame frame, final EVM evm) {
+ return new OperationResult(GAS_COST, null);
+ }
+ };
+ private static MockedStatic contextMockedStatic;
+
+ @Spy
+ private ContractCallContext contractCallContext;
+
+ @Mock
+ private ProxyWorldUpdater worldUpdater;
+
+ @Mock
+ private MutableAccount recipientAccount;
+
+ @Mock
+ private PrecompilesHolder precompilesHolder;
+
+ @Mock
+ private MirrorNodeEvmProperties mirrorNodeEvmProperties;
+
+ // Transient test data
+ private OpcodeActionTracer tracer;
+ private OpcodeTracerOptions tracerOptions;
+ private MessageFrame frame;
+
+ // EVM data for capture
+ private UInt256[] stackItems;
+ private Bytes[] wordsInMemory;
+ private Map updatedStorage;
+
+ @BeforeAll
+ static void initStaticMocks() {
+ contextMockedStatic = mockStatic(ContractCallContext.class);
+ }
+
+ @AfterAll
+ static void closeStaticMocks() {
+ contextMockedStatic.close();
+ }
+
+ @BeforeEach
+ void setUp() {
+ when(precompilesHolder.getHederaPrecompiles())
+ .thenReturn(Map.of(
+ HTS_PRECOMPILE_ADDRESS.toString(),
+ mock(PrecompiledContract.class),
+ PRNG_PRECOMPILE_ADDRESS,
+ mock(PrecompiledContract.class),
+ EXCHANGE_RATE_SYSTEM_CONTRACT_ADDRESS,
+ mock(PrecompiledContract.class)));
+ REMAINING_GAS.set(INITIAL_GAS);
+ tracer = new OpcodeActionTracer(precompilesHolder);
+ tracerOptions = new OpcodeTracerOptions(false, false, false, true);
+ contextMockedStatic.when(ContractCallContext::get).thenReturn(contractCallContext);
+ }
+
+ @AfterEach
+ void tearDown() {
+ verifyMocks();
+ reset(contractCallContext);
+ reset(worldUpdater);
+ reset(recipientAccount);
+ }
+
+ private void verifyMocks() {
+ if (tracerOptions.isStorage()) {
+ try {
+ MutableAccount account = worldUpdater.getAccount(frame.getRecipientAddress());
+ if (account != null) {
+ assertThat(account).isEqualTo(recipientAccount);
+ if (!mirrorNodeEvmProperties.isModularizedServices()) {
+ verify(recipientAccount, times(1)).getUpdatedStorage();
+ }
+ }
+ } catch (final ModificationNotAllowedException e) {
+ if (!mirrorNodeEvmProperties.isModularizedServices()) {
+ verify(recipientAccount, never()).getUpdatedStorage();
+ }
+ }
+ } else {
+ verify(worldUpdater, never()).getAccount(any());
+ if (!mirrorNodeEvmProperties.isModularizedServices()) {
+ verify(recipientAccount, never()).getUpdatedStorage();
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("should record program counter")
+ void shouldRecordProgramCounter() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.pc()).isEqualTo(frame.getPC());
+ }
+
+ @Test
+ @DisplayName("should record opcode")
+ void shouldRecordOpcode() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.op()).isNotEmpty();
+ assertThat(opcode.op()).contains(OPERATION.getName());
+ }
+
+ @Test
+ @DisplayName("should record depth")
+ void shouldRecordDepth() {
+ frame = setupInitialFrame(tracerOptions);
+
+ // simulate 4 calls
+ final int expectedDepth = 4;
+ for (int i = 0; i < expectedDepth; i++) {
+ frame.getMessageFrameStack()
+ .add(buildMessageFrame(Address.fromHexString("0x10%d".formatted(i)), MESSAGE_CALL));
+ }
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.depth()).isEqualTo(expectedDepth);
+ }
+
+ @Test
+ @DisplayName("should record remaining gas")
+ void shouldRecordRemainingGas() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.gas()).isEqualTo(REMAINING_GAS.get());
+ }
+
+ @Test
+ @DisplayName("should record gas cost")
+ void shouldRecordGasCost() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.gasCost()).isEqualTo(GAS_COST);
+ }
+
+ @Test
+ @DisplayName("given stack is enabled in tracer options, should record stack")
+ void shouldRecordStackWhenEnabled() {
+ tracerOptions = tracerOptions.toBuilder().stack(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.stack()).isNotEmpty();
+ assertThat(opcode.stack()).containsExactly(stackItems);
+ }
+
+ @Test
+ @DisplayName("given stack is disabled in tracer options, should not record stack")
+ void shouldNotRecordStackWhenDisabled() {
+ tracerOptions = tracerOptions.toBuilder().stack(false).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.stack()).isEmpty();
+ }
+
+ @Test
+ @DisplayName("given memory is enabled in tracer options, should record memory")
+ void shouldRecordMemoryWhenEnabled() {
+ tracerOptions = tracerOptions.toBuilder().memory(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.memory()).isNotEmpty();
+ assertThat(opcode.memory()).containsExactly(wordsInMemory);
+ }
+
+ @Test
+ @DisplayName("given memory is disabled in tracer options, should not record memory")
+ void shouldNotRecordMemoryWhenDisabled() {
+ tracerOptions =
+ tracerOptions.toBuilder().memory(false).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.memory()).isEmpty();
+ }
+
+ @Test
+ @DisplayName("given storage is enabled in tracer options, should record storage")
+ void shouldRecordStorageWhenEnabled() {
+ tracerOptions =
+ tracerOptions.toBuilder().storage(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.storage()).isNotEmpty();
+ assertThat(opcode.storage()).containsAllEntriesOf(updatedStorage);
+ }
+
+ @Test
+ @DisplayName("given storage is enabled in tracer options, should record storage for modularized services")
+ void shouldRecordStorageWhenEnabledModularized() {
+ // Given
+ tracerOptions =
+ tracerOptions.toBuilder().storage(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ // Expected storage map
+ final Map expectedStorage = ImmutableSortedMap.of(UInt256.ZERO, UInt256.valueOf(233));
+
+ // When
+ final Opcode opcode = executeOperation(frame);
+
+ // Then
+ assertThat(opcode.storage()).isNotEmpty().containsAllEntriesOf(expectedStorage);
+ }
+
+ @Test
+ @DisplayName(
+ "given storage is enabled in tracer options, should return empty storage when there are no updates for modularized services")
+ void shouldReturnEmptyStorageWhenThereAreNoUpdates() {
+ // Given
+ tracerOptions =
+ tracerOptions.toBuilder().storage(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ when(worldUpdater.pendingStorageUpdates()).thenReturn(new ArrayList<>());
+
+ // When
+ final Opcode opcode = executeOperation(frame);
+
+ // Then
+ assertThat(opcode.storage()).isEmpty();
+ }
+
+ @Test
+ @DisplayName("given account is missing in the world updater, should only log a warning and return empty storage")
+ void shouldNotThrowExceptionWhenAccountIsMissingInWorldUpdater() {
+ tracerOptions =
+ tracerOptions.toBuilder().storage(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ when(worldUpdater.pendingStorageUpdates()).thenReturn(new ArrayList<>());
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.storage()).containsExactlyEntriesOf(new TreeMap<>());
+ }
+
+ @Test
+ @DisplayName(
+ "given ModificationNotAllowedException thrown when trying to get storage changes through WorldUpdater, "
+ + "should only log a warning and return empty storage")
+ void shouldNotThrowExceptionWhenWorldUpdaterThrowsModificationNotAllowedException() {
+ tracerOptions =
+ tracerOptions.toBuilder().storage(true).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ when(worldUpdater.pendingStorageUpdates()).thenThrow(new ModificationNotAllowedException());
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.storage()).containsExactlyEntriesOf(new TreeMap<>());
+ }
+
+ @Test
+ @DisplayName("given storage is disabled in tracer options, should not record storage")
+ void shouldNotRecordStorageWhenDisabled() {
+ tracerOptions =
+ tracerOptions.toBuilder().storage(false).modularized(true).build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.storage()).isEmpty();
+ }
+
+ @Test
+ @DisplayName("given exceptional halt occurs, should capture frame data and halt reason")
+ void shouldCaptureFrameWhenExceptionalHaltOccurs() {
+ tracerOptions = tracerOptions.toBuilder()
+ .stack(true)
+ .memory(true)
+ .storage(true)
+ .modularized(true)
+ .build();
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executeOperation(frame, INSUFFICIENT_GAS);
+ assertThat(opcode.reason())
+ .contains(Hex.encodeHexString(INSUFFICIENT_GAS.getDescription().getBytes()));
+ assertThat(opcode.stack()).contains(stackItems);
+ assertThat(opcode.memory()).contains(wordsInMemory);
+ assertThat(opcode.storage()).containsExactlyEntriesOf(updatedStorage);
+ }
+
+ @Test
+ @DisplayName("should capture a precompile call")
+ void shouldCaptureFrameWhenSuccessfulPrecompileCallOccurs() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcode.pc()).isEqualTo(frame.getPC());
+ assertThat(opcode.op()).isNotEmpty().isEqualTo(OPERATION.getName());
+ assertThat(opcode.gas()).isEqualTo(REMAINING_GAS.get());
+ assertThat(opcode.gasCost()).isEqualTo(GAS_REQUIREMENT);
+ assertThat(opcode.depth()).isEqualTo(frame.getDepth());
+ assertThat(opcode.stack()).isEmpty();
+ assertThat(opcode.memory()).isEmpty();
+ assertThat(opcode.storage()).isEmpty();
+ assertThat(opcode.reason())
+ .isEqualTo(frame.getRevertReason().map(Bytes::toString).orElse(null));
+ }
+
+ @Test
+ @DisplayName("should not record gas requirement of precompile call with null output")
+ void shouldNotRecordGasRequirementWhenPrecompileCallHasNullOutput() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executePrecompileOperation(frame, GAS_REQUIREMENT, Bytes.EMPTY);
+ assertThat(opcode.gasCost()).isZero();
+ }
+
+ @Test
+ @DisplayName("should not record revert reason of precompile call with no revert reason")
+ void shouldNotRecordRevertReasonWhenPrecompileCallHasNoRevertReason() {
+ frame = setupInitialFrame(tracerOptions);
+
+ final Opcode opcode = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcode.reason()).isNull();
+ }
+
+ @Test
+ @DisplayName("should record revert reason of precompile call when frame has revert reason")
+ void shouldRecordRevertReasonWhenEthPrecompileCallHasRevertReason() {
+ frame = setupInitialFrame(tracerOptions, ETH_PRECOMPILE_ADDRESS, MESSAGE_CALL);
+ frame.setRevertReason(Bytes.of("revert reason".getBytes()));
+
+ final Opcode opcode = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcode.reason())
+ .isNotEmpty()
+ .isEqualTo(frame.getRevertReason().map(Bytes::toString).orElseThrow());
+ }
+
+ @Test
+ @DisplayName("should return ABI-encoded revert reason for precompile call with plaintext revert reason")
+ void shouldReturnAbiEncodedRevertReasonWhenPrecompileCallHasContractActionWithPlaintextRevertReason() {
+ final var contractActionNoRevert =
+ contractAction(0, 0, CallOperationType.OP_CREATE, OUTPUT.getNumber(), CONTRACT_ADDRESS);
+ final var contractActionWithRevert =
+ contractAction(1, 1, CallOperationType.OP_CALL, REVERT_REASON.getNumber(), HTS_PRECOMPILE_ADDRESS);
+ contractActionWithRevert.setResultData("revert reason".getBytes());
+
+ frame = setupInitialFrame(
+ tracerOptions, CONTRACT_ADDRESS, MESSAGE_CALL, contractActionNoRevert, contractActionWithRevert);
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.reason()).isNull();
+
+ final var frameOfPrecompileCall = buildMessageFrameFromAction(contractActionWithRevert);
+ frame = setupFrame(frameOfPrecompileCall);
+
+ final Opcode opcodeForPrecompileCall = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcodeForPrecompileCall.reason())
+ .isNotEmpty()
+ .isEqualTo(getAbiEncodedRevertReason(Bytes.of(contractActionWithRevert.getResultData()))
+ .toHexString());
+ }
+
+ @Test
+ @DisplayName("should return ABI-encoded revert reason for precompile call with response code for revert reason")
+ void shouldReturnAbiEncodedRevertReasonWhenPrecompileCallHasContractActionWithResponseCodeNumberRevertReason() {
+ final var contractActionNoRevert =
+ contractAction(0, 0, CallOperationType.OP_CREATE, OUTPUT.getNumber(), CONTRACT_ADDRESS);
+ final var contractActionWithRevert =
+ contractAction(1, 1, CallOperationType.OP_CALL, REVERT_REASON.getNumber(), HTS_PRECOMPILE_ADDRESS);
+ contractActionWithRevert.setResultData(ByteBuffer.allocate(32)
+ .putInt(28, ResponseCodeEnum.INVALID_ACCOUNT_ID.getNumber())
+ .array());
+
+ frame = setupInitialFrame(
+ tracerOptions, CONTRACT_ADDRESS, MESSAGE_CALL, contractActionNoRevert, contractActionWithRevert);
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.reason()).isNull();
+
+ final var frameOfPrecompileCall = buildMessageFrameFromAction(contractActionWithRevert);
+ frame = setupFrame(frameOfPrecompileCall);
+
+ final Opcode opcodeForPrecompileCall = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcodeForPrecompileCall.reason())
+ .isNotEmpty()
+ .isEqualTo(getAbiEncodedRevertReason(Bytes.of(
+ ResponseCodeEnum.INVALID_ACCOUNT_ID.name().getBytes()))
+ .toHexString());
+ }
+
+ @Test
+ @DisplayName("should return ABI-encoded revert reason for precompile call with ABI-encoded revert reason")
+ void shouldReturnAbiEncodedRevertReasonWhenPrecompileCallHasContractActionWithAbiEncodedRevertReason() {
+ final var contractActionNoRevert =
+ contractAction(0, 0, CallOperationType.OP_CREATE, OUTPUT.getNumber(), CONTRACT_ADDRESS);
+ final var contractActionWithRevert =
+ contractAction(1, 1, CallOperationType.OP_CALL, REVERT_REASON.getNumber(), HTS_PRECOMPILE_ADDRESS);
+ contractActionWithRevert.setResultData(
+ getAbiEncodedRevertReason(Bytes.of(INVALID_OPERATION.name().getBytes()))
+ .toArray());
+
+ frame = setupInitialFrame(
+ tracerOptions, CONTRACT_ADDRESS, MESSAGE_CALL, contractActionNoRevert, contractActionWithRevert);
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.reason()).isNull();
+
+ final var frameOfPrecompileCall = buildMessageFrameFromAction(contractActionWithRevert);
+ frame = setupFrame(frameOfPrecompileCall);
+
+ final Opcode opcodeForPrecompileCall = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcodeForPrecompileCall.reason())
+ .isNotEmpty()
+ .isEqualTo(Bytes.of(contractActionWithRevert.getResultData()).toHexString());
+ }
+
+ @Test
+ @DisplayName("should return empty revert reason of precompile call with empty revert reason")
+ void shouldReturnEmptyReasonWhenPrecompileCallHasContractActionWithEmptyRevertReason() {
+ final var contractActionNoRevert =
+ contractAction(0, 0, CallOperationType.OP_CREATE, OUTPUT.getNumber(), CONTRACT_ADDRESS);
+ final var contractActionWithRevert =
+ contractAction(1, 1, CallOperationType.OP_CALL, REVERT_REASON.getNumber(), HTS_PRECOMPILE_ADDRESS);
+ contractActionWithRevert.setResultData(Bytes.EMPTY.toArray());
+
+ frame = setupInitialFrame(
+ tracerOptions, CONTRACT_ADDRESS, MESSAGE_CALL, contractActionNoRevert, contractActionWithRevert);
+ final Opcode opcode = executeOperation(frame);
+ assertThat(opcode.reason()).isNull();
+
+ final var frameOfPrecompileCall = buildMessageFrameFromAction(contractActionWithRevert);
+ frame = setupFrame(frameOfPrecompileCall);
+
+ final Opcode opcodeForPrecompileCall = executePrecompileOperation(frame, GAS_REQUIREMENT, DEFAULT_OUTPUT);
+ assertThat(opcodeForPrecompileCall.reason()).isNotNull().isEqualTo(Bytes.EMPTY.toHexString());
+ }
+
+ private Opcode executeOperation(final MessageFrame frame) {
+ return executeOperation(frame, null);
+ }
+
+ private Opcode executeOperation(final MessageFrame frame, final ExceptionalHaltReason haltReason) {
+ if (frame.getState() == NOT_STARTED) {
+ tracer.traceContextEnter(frame);
+ } else {
+ tracer.traceContextReEnter(frame);
+ }
+
+ final OperationResult operationResult;
+ if (haltReason != null) {
+ frame.setState(EXCEPTIONAL_HALT);
+ frame.setRevertReason(Bytes.of(haltReason.getDescription().getBytes()));
+ operationResult = new OperationResult(GAS_COST, haltReason);
+ } else {
+ operationResult = OPERATION.execute(frame, null);
+ }
+
+ tracer.tracePostExecution(frame, operationResult);
+ if (frame.getState() == COMPLETED_SUCCESS || frame.getState() == COMPLETED_FAILED) {
+ tracer.traceContextExit(frame);
+ }
+
+ EXECUTED_FRAMES.set(EXECUTED_FRAMES.get() + 1);
+ Opcode expectedOpcode = contractCallContext.getOpcodes().get(EXECUTED_FRAMES.get() - 1);
+
+ verify(contractCallContext, times(1)).addOpcodes(expectedOpcode);
+ assertThat(contractCallContext.getOpcodes()).hasSize(1);
+ assertThat(contractCallContext.getContractActions()).isNotNull();
+ return expectedOpcode;
+ }
+
+ private Opcode executePrecompileOperation(final MessageFrame frame, final long gasRequirement, final Bytes output) {
+ if (frame.getState() == NOT_STARTED) {
+ tracer.traceContextEnter(frame);
+ } else {
+ tracer.traceContextReEnter(frame);
+ }
+ tracer.tracePrecompileCall(frame, gasRequirement, output);
+ if (frame.getState() == COMPLETED_SUCCESS || frame.getState() == COMPLETED_FAILED) {
+ tracer.traceContextExit(frame);
+ }
+
+ EXECUTED_FRAMES.set(EXECUTED_FRAMES.get() + 1);
+ Opcode expectedOpcode = contractCallContext.getOpcodes().get(EXECUTED_FRAMES.get() - 1);
+
+ verify(contractCallContext, times(1)).addOpcodes(expectedOpcode);
+ assertThat(contractCallContext.getOpcodes()).hasSize(EXECUTED_FRAMES.get());
+ assertThat(contractCallContext.getContractActions()).isNotNull();
+ return expectedOpcode;
+ }
+
+ private MessageFrame setupInitialFrame(final OpcodeTracerOptions options) {
+ return setupInitialFrame(options, CONTRACT_ADDRESS, MESSAGE_CALL);
+ }
+
+ private MessageFrame setupInitialFrame(
+ final OpcodeTracerOptions options,
+ final Address recipientAddress,
+ final MessageFrame.Type type,
+ final ContractAction... contractActions) {
+ contractCallContext.setOpcodeTracerOptions(options);
+ contractCallContext.setContractActions(Lists.newArrayList(contractActions));
+ EXECUTED_FRAMES.set(0);
+
+ final MessageFrame messageFrame = buildMessageFrame(recipientAddress, type);
+ messageFrame.setState(NOT_STARTED);
+ return setupFrame(messageFrame);
+ }
+
+ private MessageFrame setupFrame(final MessageFrame messageFrame) {
+ reset(contractCallContext);
+ stackItems = setupStackForCapture(messageFrame);
+ wordsInMemory = setupMemoryForCapture(messageFrame);
+ updatedStorage = setupStorageForCapture();
+ return messageFrame;
+ }
+
+ private Map setupStorageForCapture() {
+ final Map storage = ImmutableSortedMap.of(
+ UInt256.ZERO, UInt256.valueOf(233),
+ UInt256.ONE, UInt256.valueOf(2424));
+ final var storageAccesses = new ArrayList();
+ final var nestedStorageAccesses = new ArrayList();
+
+ final var nestedStorageAccess1 = new StorageAccess(UInt256.ZERO, UInt256.valueOf(233), UInt256.ONE);
+ final var nestedStorageAccess2 = new StorageAccess(UInt256.ONE, UInt256.valueOf(2424), UInt256.ONE);
+ nestedStorageAccesses.add(nestedStorageAccess1);
+ nestedStorageAccesses.add(nestedStorageAccess2);
+ final var storageAccess = new StorageAccesses(ContractID.DEFAULT, nestedStorageAccesses);
+ storageAccesses.add(storageAccess);
+ when(worldUpdater.pendingStorageUpdates()).thenReturn(storageAccesses);
+ return storage;
+ }
+
+ private UInt256[] setupStackForCapture(final MessageFrame frame) {
+ final UInt256[] stack = new UInt256[] {
+ UInt256.fromHexString("0x01"), UInt256.fromHexString("0x02"), UInt256.fromHexString("0x03")
+ };
+
+ for (final UInt256 stackItem : stack) {
+ frame.pushStackItem(stackItem);
+ }
+
+ return stack;
+ }
+
+ private Bytes[] setupMemoryForCapture(final MessageFrame frame) {
+ final Bytes[] words = new Bytes[] {
+ Bytes.fromHexString("0x01", 32), Bytes.fromHexString("0x02", 32), Bytes.fromHexString("0x03", 32)
+ };
+
+ for (int i = 0; i < words.length; i++) {
+ frame.writeMemory(i * 32, 32, words[i]);
+ }
+
+ return words;
+ }
+
+ private MessageFrame buildMessageFrameFromAction(ContractAction action) {
+ final var recipientAddress = Address.wrap(Bytes.of(action.getRecipientAddress()));
+ final var senderAddress = toAddress(action.getCaller());
+ final var value = Wei.of(action.getValue());
+ final var type = action.getCallType() == ContractActionType.CREATE_VALUE ? CONTRACT_CREATION : MESSAGE_CALL;
+ final var messageFrame = messageFrameBuilder(recipientAddress, type)
+ .sender(senderAddress)
+ .originator(senderAddress)
+ .address(recipientAddress)
+ .contract(recipientAddress)
+ .inputData(Bytes.of(action.getInput()))
+ .initialGas(REMAINING_GAS.get())
+ .value(value)
+ .apparentValue(value)
+ .build();
+ messageFrame.setState(NOT_STARTED);
+ return messageFrame;
+ }
+
+ private MessageFrame buildMessageFrame(final Address recipientAddress, final MessageFrame.Type type) {
+ REMAINING_GAS.set(REMAINING_GAS.get() - GAS_COST);
+
+ final MessageFrame messageFrame =
+ messageFrameBuilder(recipientAddress, type).build();
+ messageFrame.setCurrentOperation(OPERATION);
+ messageFrame.setPC(0);
+ messageFrame.setGasRemaining(REMAINING_GAS.get());
+ return messageFrame;
+ }
+
+ private MessageFrame.Builder messageFrameBuilder(final Address recipientAddress, final MessageFrame.Type type) {
+ return new MessageFrame.Builder()
+ .type(type)
+ .code(CodeV0.EMPTY_CODE)
+ .sender(Address.ZERO)
+ .originator(Address.ZERO)
+ .completer(ignored -> {})
+ .miningBeneficiary(Address.ZERO)
+ .address(recipientAddress)
+ .contract(recipientAddress)
+ .inputData(Bytes.EMPTY)
+ .initialGas(INITIAL_GAS)
+ .value(Wei.ZERO)
+ .apparentValue(Wei.ZERO)
+ .worldUpdater(worldUpdater)
+ .gasPrice(Wei.of(GAS_PRICE))
+ .blockValues(mock(BlockValues.class))
+ .blockHashLookup(ignored -> Hash.wrap(Bytes32.ZERO))
+ .contextVariables(Map.of(ContractCallContext.CONTEXT_NAME, contractCallContext));
+ }
+
+ private ContractAction contractAction(
+ final int index,
+ final int depth,
+ final CallOperationType callOperationType,
+ final int resultDataType,
+ final Address recipientAddress) {
+ return ContractAction.builder()
+ .callDepth(depth)
+ .caller(EntityId.of("0.0.1"))
+ .callerType(EntityType.ACCOUNT)
+ .callOperationType(callOperationType.getNumber())
+ .callType(ContractActionType.PRECOMPILE.getNumber())
+ .consensusTimestamp(new SecureRandom().nextLong())
+ .gas(REMAINING_GAS.get())
+ .gasUsed(GAS_PRICE)
+ .index(index)
+ .input(new byte[0])
+ .payerAccountId(EntityId.of("0.0.2"))
+ .recipientAccount(EntityId.of("0.0.3"))
+ .recipientAddress(recipientAddress.toArray())
+ .recipientContract(EntityId.of("0.0.4"))
+ .resultData(resultDataType == REVERT_REASON.getNumber() ? "revert reason".getBytes() : new byte[0])
+ .resultDataType(resultDataType)
+ .value(1L)
+ .build();
+ }
+}
diff --git a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracerTest.java b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracerTest.java
index 50791e23a6b..f4ee486117d 100644
--- a/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracerTest.java
+++ b/web3/src/test/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/OpcodeTracerTest.java
@@ -2,7 +2,6 @@
package org.hiero.mirror.web3.evm.contracts.execution.traceability;
-import static com.hedera.node.app.service.evm.contracts.operations.HederaExceptionalHaltReason.INVALID_SOLIDITY_ADDRESS;
import static com.hedera.services.store.contracts.precompile.ExchangeRatePrecompiledContract.EXCHANGE_RATE_SYSTEM_CONTRACT_ADDRESS;
import static com.hedera.services.store.contracts.precompile.PrngSystemPrecompiledContract.PRNG_PRECOMPILE_ADDRESS;
import static com.hedera.services.store.contracts.precompile.SyntheticTxnFactory.HTS_PRECOMPILED_CONTRACT_ADDRESS;
@@ -13,7 +12,6 @@
import static org.hiero.mirror.web3.evm.utils.EvmTokenUtils.toAddress;
import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INSUFFICIENT_GAS;
import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INVALID_OPERATION;
-import static org.hyperledger.besu.evm.frame.MessageFrame.State.CODE_SUSPENDED;
import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_FAILED;
import static org.hyperledger.besu.evm.frame.MessageFrame.State.COMPLETED_SUCCESS;
import static org.hyperledger.besu.evm.frame.MessageFrame.State.EXCEPTIONAL_HALT;
@@ -22,8 +20,6 @@
import static org.hyperledger.besu.evm.frame.MessageFrame.Type.MESSAGE_CALL;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -33,19 +29,12 @@
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableSortedMap;
-import com.hedera.hapi.node.state.contract.SlotKey;
-import com.hedera.hapi.node.state.contract.SlotValue;
-import com.hedera.node.app.service.contract.ContractService;
import com.hedera.services.stream.proto.CallOperationType;
import com.hedera.services.stream.proto.ContractActionType;
import com.hederahashgraph.api.proto.java.ResponseCodeEnum;
-import com.swirlds.state.spi.WritableKVState;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
-import java.util.Collections;
import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
@@ -58,13 +47,9 @@
import org.hiero.mirror.common.domain.contract.ContractAction;
import org.hiero.mirror.common.domain.entity.EntityId;
import org.hiero.mirror.common.domain.entity.EntityType;
-import org.hiero.mirror.common.util.DomainUtils;
import org.hiero.mirror.web3.common.ContractCallContext;
import org.hiero.mirror.web3.evm.config.PrecompilesHolder;
import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
-import org.hiero.mirror.web3.state.MirrorNodeState;
-import org.hiero.mirror.web3.state.core.MapWritableStates;
-import org.hiero.mirror.web3.state.keyvalue.ContractStorageReadableKVState;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
@@ -87,9 +72,7 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Spy;
@@ -100,7 +83,6 @@
@ExtendWith(MockitoExtension.class)
class OpcodeTracerTest {
- private static final String CONTRACT_SERVICE = ContractService.NAME;
private static final Address CONTRACT_ADDRESS = Address.fromHexString("0x123");
private static final Address ETH_PRECOMPILE_ADDRESS = Address.fromHexString("0x01");
private static final Address HTS_PRECOMPILE_ADDRESS = Address.fromHexString(HTS_PRECOMPILED_CONTRACT_ADDRESS);
@@ -133,9 +115,6 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) {
@Mock
private MirrorNodeEvmProperties mirrorNodeEvmProperties;
- @Mock
- private MirrorNodeState mirrorNodeState;
-
// Transient test data
private OpcodeTracer tracer;
private OpcodeTracerOptions tracerOptions;
@@ -172,7 +151,7 @@ void setUp() {
EXCHANGE_RATE_SYSTEM_CONTRACT_ADDRESS,
mock(PrecompiledContract.class)));
REMAINING_GAS.set(INITIAL_GAS);
- tracer = new OpcodeTracer(precompilesHolder, mirrorNodeEvmProperties, mirrorNodeState);
+ tracer = new OpcodeTracer(precompilesHolder);
tracerOptions = new OpcodeTracerOptions(false, false, false, false);
contextMockedStatic.when(ContractCallContext::get).thenReturn(contractCallContext);
}
@@ -210,166 +189,6 @@ private void verifyMocks() {
}
}
- @Test
- @DisplayName("should increment contract action index on init()")
- void shouldIncrementContractActionIndexOnInit() {
- frame = setupInitialFrame(tracerOptions);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.init(frame);
-
- verify(contractCallContext, times(1)).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isZero();
- }
-
- @ParameterizedTest
- @MethodSource("allMessageFrameTypesAndStates")
- @DisplayName("should increment contract action index on tracePostExecution() for SUSPENDED frame")
- void shouldIncrementContractActionIndexOnTraceContextReEnter(
- final MessageFrame.Type type, final MessageFrame.State state) {
- frame = setupInitialFrame(tracerOptions, type);
- frame.setState(state);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.tracePostExecution(frame, OPERATION.execute(frame, null));
-
- if (state == CODE_SUSPENDED) {
- verify(contractCallContext, times(1)).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isZero();
- } else {
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isEqualTo(-1);
- }
- }
-
- @ParameterizedTest
- @MethodSource("allMessageFrameTypesAndStates")
- @DisplayName("should increment contract action index for synthetic actions on traceAccountCreationResult()")
- void shouldIncrementContractActionIndexForSyntheticActionsOnAccountCreationResult(
- final MessageFrame.Type type, final MessageFrame.State state) {
- frame = setupInitialFrame(tracerOptions, type);
- frame.setState(state);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- if (type == MESSAGE_CALL && (state == EXCEPTIONAL_HALT || state == COMPLETED_FAILED)) {
- frame.setExceptionalHaltReason(Optional.of(INVALID_SOLIDITY_ADDRESS));
-
- tracer.traceAccountCreationResult(frame, frame.getExceptionalHaltReason());
-
- verify(contractCallContext, times(1)).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isZero();
- } else {
- tracer.traceAccountCreationResult(frame, frame.getExceptionalHaltReason());
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isEqualTo(-1);
- }
- }
-
- @ParameterizedTest
- @MethodSource("allMessageFrameTypesAndStates")
- @DisplayName("should not increment contract action index when halt reason is empty on traceAccountCreationResult()")
- void shouldNotIncrementContractActionIndexForEmptyHaltReasonOnTraceAccountCreationResult(
- final MessageFrame.Type type, final MessageFrame.State state) {
- frame = setupInitialFrame(tracerOptions, type);
- frame.setState(state);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.traceAccountCreationResult(frame, Optional.empty());
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isEqualTo(-1);
- }
-
- @ParameterizedTest
- @MethodSource("allMessageFrameTypesAndStates")
- @DisplayName("should not increment contract action index when halt reason is not INVALID_SOLIDITY_ADDRESS "
- + "on traceAccountCreationResult()")
- void shouldNotIncrementContractActionIndexForHaltReasonNotOfSyntheticActionOnTraceAccountCreationResult(
- final MessageFrame.Type type, final MessageFrame.State state) {
- frame = setupInitialFrame(tracerOptions, type);
- frame.setState(state);
- frame.setExceptionalHaltReason(Optional.of(INSUFFICIENT_GAS));
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.traceAccountCreationResult(frame, Optional.of(INSUFFICIENT_GAS));
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isEqualTo(-1);
- }
-
- @ParameterizedTest
- @MethodSource("allMessageFrameTypesAndStates")
- @DisplayName("should increment contract action index for synthetic actions on tracePrecompileResult()")
- void shouldIncrementContractActionIndexForSyntheticActionsOnTracePrecompileResult(
- final MessageFrame.Type type, final MessageFrame.State state) {
- frame = setupInitialFrame(tracerOptions, type);
- frame.setState(state);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- if (type == MESSAGE_CALL && (state == EXCEPTIONAL_HALT || state == COMPLETED_FAILED)) {
- frame.setExceptionalHaltReason(Optional.of(INVALID_SOLIDITY_ADDRESS));
-
- tracer.tracePrecompileResult(frame, ContractActionType.SYSTEM);
-
- verify(contractCallContext, times(1)).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isZero();
- } else {
- tracer.tracePrecompileResult(frame, ContractActionType.SYSTEM);
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame())
- .isEqualTo(-1);
- }
- }
-
- @Test
- @DisplayName("should not increment contract action index for halted precompile frames on tracePrecompileCall()")
- void shouldNotIncrementContractActionIndexForHaltedPrecompileFrameOnTracePrecompileResult() {
- frame = setupInitialFrame(tracerOptions, ETH_PRECOMPILE_ADDRESS, MESSAGE_CALL);
- frame.setState(EXCEPTIONAL_HALT);
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.tracePrecompileResult(frame, ContractActionType.PRECOMPILE);
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isEqualTo(-1);
- }
-
- @Test
- @DisplayName("should not increment contract action index when halt reason is empty on tracePrecompileCall()")
- void shouldNotIncrementContractActionIndexForEmptyHaltReasonOnTracePrecompileResult() {
- frame = setupInitialFrame(tracerOptions, ETH_PRECOMPILE_ADDRESS, MESSAGE_CALL);
- frame.setState(EXCEPTIONAL_HALT);
- frame.setExceptionalHaltReason(Optional.empty());
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.tracePrecompileResult(frame, ContractActionType.SYSTEM);
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isEqualTo(-1);
- }
-
- @Test
- @DisplayName("should not increment contract action index when halt reason is not INVALID_SOLIDITY_ADDRESS "
- + "on tracePrecompileCall()")
- void shouldNotIncrementContractActionIndexForHaltReasonNotOfSynthenticActionOnTracePrecompileResult() {
- frame = setupInitialFrame(tracerOptions, ETH_PRECOMPILE_ADDRESS, MESSAGE_CALL);
- frame.setState(EXCEPTIONAL_HALT);
- frame.setExceptionalHaltReason(Optional.of(INSUFFICIENT_GAS));
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
-
- tracer.tracePrecompileResult(frame, ContractActionType.SYSTEM);
-
- verify(contractCallContext, never()).incrementContractActionsCounter();
- assertThat(contractCallContext.getContractActionIndexOfCurrentFrame()).isEqualTo(-1);
- }
-
@Test
@DisplayName("should record program counter")
void shouldRecordProgramCounter() {
@@ -476,141 +295,6 @@ void shouldRecordStorageWhenEnabled() {
assertThat(opcode.storage()).containsAllEntriesOf(updatedStorage);
}
- @Test
- @DisplayName("given storage is enabled in tracer options, should record storage for modularized services")
- void shouldRecordStorageWhenEnabledModularized() {
- // Given
- tracerOptions =
- tracerOptions.toBuilder().storage(true).modularized(true).build();
- when(mirrorNodeEvmProperties.isModularizedServices()).thenReturn(true);
- frame = setupInitialFrame(tracerOptions);
-
- // Mock writable states
- MapWritableStates mockStates = mock(MapWritableStates.class);
- when(mirrorNodeState.getWritableStates(CONTRACT_SERVICE)).thenReturn(mockStates);
- WritableKVState mockStorageState = mock(WritableKVState.class);
- doReturn(mockStorageState).when(mockStates).get(ContractStorageReadableKVState.KEY);
-
- // Mock SlotKey and SlotValue
- SlotKey slotKey = createMockSlotKey();
- SlotValue slotValue = createMockSlotValue(UInt256.valueOf(233));
-
- // Mock modified keys retrieval
- when(mockStorageState.modifiedKeys()).thenReturn(Set.of(slotKey));
- when(mockStorageState.get(slotKey)).thenReturn(slotValue);
-
- // Expected storage map
- final Map expectedStorage = ImmutableSortedMap.of(UInt256.ZERO, UInt256.valueOf(233));
-
- // When
- final Opcode opcode = executeOperation(frame);
-
- // Then
- assertThat(opcode.storage()).isNotEmpty().containsAllEntriesOf(expectedStorage);
- }
-
- @Test
- @DisplayName(
- "given storage is enabled in tracer options, should return empty storage when there are no updates for modularized services")
- void shouldReturnEmptyStorageWhenThereAreNoUpdates() {
- // Given
- tracerOptions =
- tracerOptions.toBuilder().storage(true).modularized(true).build();
- when(mirrorNodeEvmProperties.isModularizedServices()).thenReturn(true);
- frame = setupInitialFrame(tracerOptions);
-
- // Mock writable states
- MapWritableStates mockStates = mock(MapWritableStates.class);
- when(mirrorNodeState.getWritableStates(CONTRACT_SERVICE)).thenReturn(mockStates);
- WritableKVState mockStorageState = mock(WritableKVState.class);
- doReturn(mockStorageState).when(mockStates).get(ContractStorageReadableKVState.KEY);
-
- // Mock empty modified keys retrieval
- when(mockStorageState.modifiedKeys()).thenReturn(Collections.emptySet());
-
- // When
- final Opcode opcode = executeOperation(frame);
-
- // Then
- assertThat(opcode.storage()).isEmpty();
- }
-
- @Test
- @DisplayName(
- "given storage is enabled in tracer options, should skip slotKey when contract address does not match for modularized services")
- void shouldSkipSlotKeyWhenContractAddressDoesNotMatch() {
- // Given
- tracerOptions =
- tracerOptions.toBuilder().storage(true).modularized(true).build();
- when(mirrorNodeEvmProperties.isModularizedServices()).thenReturn(true);
- frame = setupInitialFrame(tracerOptions);
-
- MapWritableStates mockStates = mock(MapWritableStates.class);
- when(mirrorNodeState.getWritableStates(CONTRACT_SERVICE)).thenReturn(mockStates);
- WritableKVState mockStorageState = mock(WritableKVState.class);
- doReturn(mockStorageState).when(mockStates).get(ContractStorageReadableKVState.KEY);
-
- SlotKey mismatchedSlotKey = createMockSlotKey(Address.fromHexString("0xDEADBEEF"));
- when(mockStorageState.modifiedKeys()).thenReturn(Set.of(mismatchedSlotKey));
-
- // When
- final Opcode opcode = executeOperation(frame);
-
- // Then
- assertThat(opcode.storage()).isEmpty();
- }
-
- @Test
- @DisplayName(
- "given storage is enabled in tracer options, should skip slotKey when slotValue is null for modularized services")
- void shouldSkipSlotKeyWhenSlotValueIsNull() {
- // Given
- tracerOptions =
- tracerOptions.toBuilder().storage(true).modularized(true).build();
- when(mirrorNodeEvmProperties.isModularizedServices()).thenReturn(true);
- frame = setupInitialFrame(tracerOptions);
-
- MapWritableStates mockStates = mock(MapWritableStates.class);
- when(mirrorNodeState.getWritableStates(CONTRACT_SERVICE)).thenReturn(mockStates);
- WritableKVState mockStorageState = mock(WritableKVState.class);
- doReturn(mockStorageState).when(mockStates).get(ContractStorageReadableKVState.KEY);
-
- SlotKey slotKey = createMockSlotKey(CONTRACT_ADDRESS);
-
- when(mockStorageState.modifiedKeys()).thenReturn(Set.of(slotKey));
- when(mockStorageState.get(slotKey)).thenReturn(null); // SlotValue is null
-
- // When
- final Opcode opcode = executeOperation(frame);
-
- // Then
- assertThat(opcode.storage()).isEmpty();
- }
-
- @Test
- @DisplayName(
- "given storage is enabled in tracer options, should return empty storage when STORAGE_KEY retrieval fails for modularized services")
- void shouldReturnEmptyStorageWhenStorageKeyRetrievalFails() {
- // Given
- tracerOptions =
- tracerOptions.toBuilder().storage(true).modularized(true).build();
- when(mirrorNodeEvmProperties.isModularizedServices()).thenReturn(true);
- frame = setupInitialFrame(tracerOptions);
-
- MapWritableStates mockStates = mock(MapWritableStates.class);
- when(mirrorNodeState.getWritableStates(CONTRACT_SERVICE)).thenReturn(mockStates);
-
- // Mock storage retrieval to throw IllegalArgumentException
- when(mockStates.get(ContractStorageReadableKVState.KEY))
- .thenThrow(new IllegalArgumentException("Storage retrieval failed"));
-
- // When
- final Opcode opcode = executeOperation(frame);
-
- // Then
- assertThat(opcode.storage()).isEmpty(); // Ensures empty storage response instead of exception
- }
-
@Test
@DisplayName("given account is missing in the world updater, should only log a warning and return empty storage")
void shouldNotThrowExceptionWhenAccountIsMissingInWorldUpdater() {
@@ -837,7 +521,6 @@ private Opcode executeOperation(final MessageFrame frame, final ExceptionalHaltR
Opcode expectedOpcode = contractCallContext.getOpcodes().get(EXECUTED_FRAMES.get() - 1);
verify(contractCallContext, times(1)).addOpcodes(expectedOpcode);
- assertThat(tracer.getContext().getOpcodeTracerOptions()).isEqualTo(tracerOptions);
assertThat(contractCallContext.getOpcodes()).hasSize(1);
assertThat(contractCallContext.getContractActions()).isNotNull();
return expectedOpcode;
@@ -860,7 +543,6 @@ private Opcode executePrecompileOperation(final MessageFrame frame, final Bytes
Opcode expectedOpcode = contractCallContext.getOpcodes().get(EXECUTED_FRAMES.get() - 1);
verify(contractCallContext, times(1)).addOpcodes(expectedOpcode);
- assertThat(tracer.getContext().getOpcodeTracerOptions()).isEqualTo(tracerOptions);
assertThat(contractCallContext.getOpcodes()).hasSize(EXECUTED_FRAMES.get());
assertThat(contractCallContext.getContractActions()).isNotNull();
return expectedOpcode;
@@ -870,10 +552,6 @@ private MessageFrame setupInitialFrame(final OpcodeTracerOptions options) {
return setupInitialFrame(options, CONTRACT_ADDRESS, MESSAGE_CALL);
}
- private MessageFrame setupInitialFrame(final OpcodeTracerOptions options, final MessageFrame.Type type) {
- return setupInitialFrame(options, CONTRACT_ADDRESS, type);
- }
-
private MessageFrame setupInitialFrame(
final OpcodeTracerOptions options,
final Address recipientAddress,
@@ -881,7 +559,6 @@ private MessageFrame setupInitialFrame(
final ContractAction... contractActions) {
contractCallContext.setOpcodeTracerOptions(options);
contractCallContext.setContractActions(Lists.newArrayList(contractActions));
- contractCallContext.setContractActionIndexOfCurrentFrame(-1);
EXECUTED_FRAMES.set(0);
final MessageFrame messageFrame = buildMessageFrame(recipientAddress, type);
@@ -1011,40 +688,4 @@ private ContractAction contractAction(
.value(1L)
.build();
}
-
- /**
- * Helper method to create a mocked SlotKey with a specified contract address. Uses lenient stubbing to prevent
- * UnnecessaryStubbingException in certain tests.
- */
- private SlotKey createMockSlotKey(Address contractAddress) {
- var slotKey = mock(SlotKey.class);
-
- var entityId = DomainUtils.fromEvmAddress(contractAddress.toArray());
- var testContractId = com.hedera.hapi.node.base.ContractID.newBuilder()
- .contractNum(entityId.getNum())
- .realmNum(entityId.getRealm())
- .shardNum(entityId.getShard())
- .build();
-
- lenient().when(slotKey.contractID()).thenReturn(testContractId);
- lenient().when(slotKey.key()).thenReturn(com.hedera.pbj.runtime.io.buffer.Bytes.wrap(UInt256.ZERO.toArray()));
-
- return slotKey;
- }
-
- /**
- * Overloaded method to create a SlotKey with the default contract address.
- */
- private SlotKey createMockSlotKey() {
- return createMockSlotKey(OpcodeTracerTest.CONTRACT_ADDRESS);
- }
-
- /**
- * Helper method to create a mocked SlotValue.
- */
- private SlotValue createMockSlotValue(UInt256 value) {
- SlotValue slotValue = mock(SlotValue.class);
- when(slotValue.value()).thenReturn(com.hedera.pbj.runtime.io.buffer.Bytes.wrap(value.toArray()));
- return slotValue;
- }
}
diff --git a/web3/src/test/java/org/hiero/mirror/web3/service/TransactionExecutionServiceTest.java b/web3/src/test/java/org/hiero/mirror/web3/service/TransactionExecutionServiceTest.java
index c12bb487a1a..b5998ec503b 100644
--- a/web3/src/test/java/org/hiero/mirror/web3/service/TransactionExecutionServiceTest.java
+++ b/web3/src/test/java/org/hiero/mirror/web3/service/TransactionExecutionServiceTest.java
@@ -29,7 +29,9 @@
import org.hiero.mirror.common.domain.SystemEntity;
import org.hiero.mirror.web3.ContextExtension;
import org.hiero.mirror.web3.common.ContractCallContext;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.MirrorOperationTracer;
+import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeActionTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracer;
import org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeTracerOptions;
import org.hiero.mirror.web3.evm.properties.MirrorNodeEvmProperties;
@@ -73,6 +75,12 @@ class TransactionExecutionServiceTest {
@Mock
private MirrorOperationTracer mirrorOperationTracer;
+ @Mock
+ private OpcodeActionTracer opcodeActionTracer;
+
+ @Mock
+ private MirrorOperationActionTracer mirrorOperationActionTracer;
+
@Mock
private TransactionExecutor transactionExecutor;
@@ -98,6 +106,8 @@ void setUp() {
new MirrorNodeEvmProperties(commonProperties, systemEntity),
opcodeTracer,
mirrorOperationTracer,
+ opcodeActionTracer,
+ mirrorOperationActionTracer,
systemEntity,
transactionExecutorFactory);
when(transactionExecutorFactory.get()).thenReturn(transactionExecutor);