diff --git a/web3/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java b/web3/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java new file mode 100644 index 00000000000..8b60796c2e5 --- /dev/null +++ b/web3/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: Apache-2.0 + +package com.hedera.node.app.service.contract.impl.exec.processors; + +import static com.hedera.hapi.streams.ContractActionType.PRECOMPILE; +import static com.hedera.hapi.streams.ContractActionType.SYSTEM; +import static com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason.INSUFFICIENT_CHILD_RECORDS; +import static com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason.INVALID_CONTRACT_ID; +import static com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason.INVALID_SIGNATURE; +import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.create.CreateCommons.createMethodsSet; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.acquiredSenderAuthorizationViaDelegateCall; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.alreadyHalted; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.incrementOpsDuration; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.isPrecompileEnabled; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.isTopLevelTransaction; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.proxyUpdaterFor; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.recordBuilderFor; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.setPropagatedCallFailure; +import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.transfersValue; +import static com.hedera.node.app.service.contract.impl.hevm.HederaOpsDuration.MULTIPLIER_FACTOR; +import static com.hedera.node.app.service.contract.impl.hevm.HevmPropagatedCallFailure.MISSING_RECEIVER_SIGNATURE; +import static com.hedera.node.app.service.contract.impl.hevm.HevmPropagatedCallFailure.RESULT_CANNOT_BE_EXTERNALIZED; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.numberOfLongZero; +import static org.hyperledger.besu.evm.frame.ExceptionalHaltReason.INSUFFICIENT_GAS; +import static org.hyperledger.besu.evm.frame.MessageFrame.State.EXCEPTIONAL_HALT; + +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.streams.ContractActionType; +import com.hedera.node.app.service.contract.impl.exec.ActionSidecarContentTracer; +import com.hedera.node.app.service.contract.impl.exec.AddressChecks; +import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.HederaSystemContract; +import com.hedera.node.app.service.contract.impl.exec.tracers.AddOnEvmActionTracer; +import com.hedera.node.app.service.contract.impl.hevm.HederaOpsDuration; +import com.hedera.node.app.service.contract.impl.state.ProxyEvmContract; +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 java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; +import org.hiero.mirror.web3.common.ContractCallContext; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.EVM; +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.PrecompileContractRegistry; +import org.hyperledger.besu.evm.precompile.PrecompiledContract; +import org.hyperledger.besu.evm.precompile.PrecompiledContract.PrecompileContractResult; +import org.hyperledger.besu.evm.processor.MessageCallProcessor; +import org.hyperledger.besu.evm.tracing.OperationTracer; + +/** + * A {@link MessageCallProcessor} customized to, + *
    + *
  1. Call Hedera-specific precompiles.
  2. + *
  3. Impose Hedera restrictions in the system account range.
  4. + *
  5. Do lazy creation when appropriate.
  6. + *
+ * Note these only require changing {@link MessageCallProcessor#start(MessageFrame, OperationTracer)}, + * and the core {@link MessageCallProcessor#process(MessageFrame, OperationTracer)} logic we inherit. + * + * Copy of the class from hedera-app. The differences with it are: + * + * - It sets the gasRequirement for a system contract in the ContractCallContext. + * - It calls {@link AddOnEvmActionTracer#tracePrecompileCall(MessageFrame, long, Bytes)} instead of + * {@link AddOnEvmActionTracer#tracePrecompileResult(MessageFrame, ContractActionType)} for precompiles. + * The reasons are that we need to pass the gasRequirement to the tracer and that + * {@link org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeActionTracer} is + * added as an addOnTracer. The {@link AddOnEvmActionTracer#tracePrecompileCall(MessageFrame, long, Bytes)} method + * delegates the call to the list of addOnTracers, where the {@link org.hiero.mirror.web3.evm.contracts.execution.traceability.OpcodeActionTracer} is stored. + */ +public class CustomMessageCallProcessor extends MessageCallProcessor { + private final FeatureFlags featureFlags; + private final AddressChecks addressChecks; + private final PrecompileContractRegistry precompiles; + private final Map systemContracts; + private final HederaOpsDuration hederaOpsDuration; + + private enum ForLazyCreation { + YES, + NO, + } + + /** + * Constructor. + * @param evm the evm to use in this call + * @param featureFlags current evm module feature flags + * @param precompiles the present precompiles + * @param addressChecks checks against addresses reserved for Hedera + * @param systemContracts the Hedera system contracts + */ + public CustomMessageCallProcessor( + @NonNull final EVM evm, + @NonNull final FeatureFlags featureFlags, + @NonNull final PrecompileContractRegistry precompiles, + @NonNull final AddressChecks addressChecks, + @NonNull final Map 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, + *

    + *
  1. A Hedera system contract.
  2. + *
  3. A native EVM precompile.
  4. + *
  5. A Hedera system account (up to {@code 0.0.750}).
  6. + *
  7. A valid lazy-creation target address.
  8. + *
  9. An existing contract.
  10. + *
  11. An existing account.
  12. + *
+ * + * @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);