Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7f6b200
feat: add tracers needed for debug_traceTransaction to be compliant w…
IvanKavaldzhiev Jun 26, 2025
a1a8f1c
fix: unit tests
IvanKavaldzhiev Jun 26, 2025
ddd1086
refactor: remove unnecessary code and extract common code in an utili…
IvanKavaldzhiev Jun 26, 2025
c03a405
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jun 26, 2025
fbc394d
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jun 30, 2025
d82f723
nit: copy CustomMessageCallProcessor from upstream
IvanKavaldzhiev Jun 30, 2025
8f25f5f
feat: add support for OpcodeActionTracer in modularized workflow
IvanKavaldzhiev Jun 30, 2025
5dd3038
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jun 30, 2025
4f89b5b
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 1, 2025
5760ce5
fix: unit tests
IvanKavaldzhiev Jul 2, 2025
50e2a5e
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 2, 2025
de03967
nit: resolve PR comments
IvanKavaldzhiev Jul 3, 2025
7ad4ec8
feat: add missing modularized header in OpcodesController
IvanKavaldzhiev Jul 4, 2025
4a9f6f8
fix: adapt opcode tracer to work with nested transactions
IvanKavaldzhiev Jul 8, 2025
40d3b4c
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 8, 2025
78fd0a9
style: add given,when,then separation in tests
IvanKavaldzhiev Jul 8, 2025
89fc1ec
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 9, 2025
e7ee181
nit: resolve PR comments
IvanKavaldzhiev Jul 9, 2025
add64cd
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 9, 2025
f07e1c5
nit: remove conditional bean declaration
IvanKavaldzhiev Jul 9, 2025
8cb1803
Merge remote-tracking branch 'origin/main' into 11424-fix-tracer-with…
IvanKavaldzhiev Jul 10, 2025
4742d1b
nit: resolve PR comments
IvanKavaldzhiev Jul 10, 2025
89e3b14
nit: add missing final statement
IvanKavaldzhiev Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,14 +41,6 @@ public class ContractCallContext {
@Setter
private List<ContractAction> 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;

Expand Down Expand Up @@ -83,6 +74,9 @@ public class ContractCallContext {
@Setter
private boolean isBalanceCall;

@Setter
private long gasRequirement;

private ContractCallContext() {}

public static ContractCallContext get() {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -248,14 +252,29 @@
}

@Bean
Map<TracerType, Provider<HederaEvmOperationTracer>> tracerProvider(
@ConditionalOnProperty(
name = "hiero.mirror.web3.evm.modularizedServices",
havingValue = "false",
matchIfMissing = true)
Map<TracerType, Provider<HederaEvmOperationTracer>> monoTracerProvider(
final MirrorOperationTracer mirrorOperationTracer, final OpcodeTracer opcodeTracer) {
Map<TracerType, Provider<HederaEvmOperationTracer>> tracerMap = new EnumMap<>(TracerType.class);
tracerMap.put(TracerType.OPCODE, () -> opcodeTracer);
tracerMap.put(TracerType.OPERATION, () -> mirrorOperationTracer);
return tracerMap;
}

@Bean
@ConditionalOnProperty(name = "hiero.mirror.web3.evm.modularizedServices", havingValue = "true")
Map<TracerType, Provider<ActionSidecarContentTracer>> tracerProvider(
final MirrorOperationActionTracer mirrorOperationActionTracer,
final OpcodeActionTracer opcodeActionTracer) {
Map<TracerType, Provider<ActionSidecarContentTracer>> tracerMap = new EnumMap<>(TracerType.class);

Check warning on line 272 in web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

web3/src/main/java/org/hiero/mirror/web3/evm/config/EvmConfiguration.java#L272

If you run in Java5 or newer and have concurrent access, you should use the ConcurrentHashMap implementation
tracerMap.put(TracerType.OPCODE, () -> opcodeActionTracer);
tracerMap.put(TracerType.OPERATION, () -> mirrorOperationActionTracer);
return tracerMap;
}

@Bean
Map<SemanticVersion, Provider<ContractCreationProcessor>> contractCreationProcessorProvider(
final ContractCreationProcessor contractCreationProcessor30,
Expand Down
Original file line number Diff line number Diff line change
@@ -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(

Check notice on line 34 in web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java#L34

Method MirrorOperationActionTracer::tracePostExecution has 34 lines of code (limit is 30)

Check notice on line 34 in web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java#L34

Method MirrorOperationActionTracer::tracePostExecution has a cyclomatic complexity of 6 (limit is 5)
@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={}",

Check notice on line 57 in web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

web3/src/main/java/org/hiero/mirror/web3/evm/contracts/execution/traceability/MirrorOperationActionTracer.java#L57

Line is longer than 120 characters (found 157).
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Address, PrecompiledContract> 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<Bytes, Bytes> 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;
}
}
Loading
Loading