Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/releases-comparator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ jobs:
"com/hedera/hapi/node/state/schedule/Schedule.java"
"com/swirlds/state/spi/ReadableKVStateBase.java"
"com/swirlds/state/spi/WritableKVStateBase.java"
"com/hedera/node/app/service/contract/impl/exec/processors/CustomMessageCallProcessor.java"
"com/hedera/node/app/service/contract/impl/state/DispatchingEvmFrameState.java"
)

# Temp directories to store cloned repos
Expand Down

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 @@ -3,8 +3,10 @@
package org.hiero.mirror.web3.controller;

import static org.hiero.mirror.web3.config.ThrottleConfiguration.RATE_LIMIT_BUCKET;
import static org.hiero.mirror.web3.utils.Constants.MODULARIZED_HEADER;

import io.github.bucket4j.Bucket;
import jakarta.servlet.http.HttpServletResponse;
import lombok.CustomLog;
import lombok.RequiredArgsConstructor;
import org.hiero.mirror.rest.model.OpcodesResponse;
Expand All @@ -17,6 +19,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -57,12 +60,23 @@ OpcodesResponse getContractOpcodes(
@PathVariable TransactionIdOrHashParameter transactionIdOrHash,
@RequestParam(required = false, defaultValue = "true") boolean stack,
@RequestParam(required = false, defaultValue = "false") boolean memory,
@RequestParam(required = false, defaultValue = "false") boolean storage) {
@RequestParam(required = false, defaultValue = "false") boolean storage,
@RequestHeader(value = MODULARIZED_HEADER, required = false) String isModularizedHeader,
HttpServletResponse response) {
if (!rateLimitBucket.tryConsume(1)) {
throw new ThrottleException("Requests per second rate limit exceeded.");
}

final boolean isModularized = evmProperties.directTrafficThroughTransactionExecutionService();
boolean isModularized = evmProperties.directTrafficThroughTransactionExecutionService();

// Temporary workaround to ensure modularized services are fully available when enabled.
// This prevents flakiness in acceptance tests, as directTrafficThroughTransactionExecutionService()
// can distribute traffic between the old and new logic.
if (isModularizedHeader != null && evmProperties.isModularizedServices()) {
isModularized = Boolean.parseBoolean(isModularizedHeader);
}

response.addHeader(MODULARIZED_HEADER, String.valueOf(isModularized));
final var options = new OpcodeTracerOptions(stack, memory, storage, isModularized);
return opcodeService.processOpcodeCall(transactionIdOrHash, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ CacheManager cacheManagerRecordFileEarliest() {
}

@Bean
Map<TracerType, Provider<HederaEvmOperationTracer>> tracerProvider(
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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.Optional;
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.evm.frame.MessageFrame;
import org.springframework.util.CollectionUtils;

public abstract class AbstractOpcodeTracer {

protected final List<Bytes> captureMemory(final MessageFrame frame, final OpcodeTracerOptions options) {
if (!options.isMemory()) {
return Collections.emptyList();
}

int size = frame.memoryWordSize();
var memory = new ArrayList<Bytes>(size);
for (int i = 0; i < size; i++) {
memory.add(frame.readMemory(i * 32L, 32));
}

return memory;
}

protected final List<Bytes> captureStack(final MessageFrame frame, final OpcodeTracerOptions options) {
if (!options.isStack()) {
return Collections.emptyList();
}

int size = frame.stackSize();
var stack = new ArrayList<Bytes>(size);
for (int i = 0; i < size; ++i) {
stack.add(frame.getStackItem(size - 1 - i));
}

return stack;
}

protected final Optional<Bytes> getRevertReasonFromContractActions(final ContractCallContext context) {
final var contractActions = context.getContractActions();

if (CollectionUtils.isEmpty(contractActions)) {
return Optional.empty();
}

return contractActions.stream()
.filter(ContractAction::hasRevertReason)
.map(action -> Bytes.of(action.getResultData()))
.map(this::formatRevertReason)
.findFirst();
}

/**
* Formats the revert reason to be consistent with the revert reason format in the EVM. <a
* href="https://besu.hyperledger.org/23.10.2/private-networks/how-to/send-transactions/revert-reason#revert-reason-format">...</a>
*
* @param revertReason the revert reason
* @return the formatted revert reason
*/
protected final 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class MirrorOperationActionTracer implements ActionSidecarContentTracer {

private final TraceProperties traceProperties;
private final 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(

Check warning on line 52 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#L52

Logger calls should be surrounded by log level guards.
"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;
}
}
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
Loading
Loading