Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0df540b
Implement Geth's 4ByteTracer in Besu
usmansaleem Jan 16, 2026
8d8b569
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 16, 2026
1b4a54e
Add CHANGELOG entry for 4byteTracer
usmansaleem Jan 16, 2026
8deee68
handle initial transaction call in 4bytetracer converter
usmansaleem Jan 19, 2026
a0b1a18
4ByteTracer - Use LinkedHashMap to keep the CALL insertion order
usmansaleem Jan 19, 2026
c461ea7
4ByteTracer - Use TreeMap to sort keys to match Geth output
usmansaleem Jan 19, 2026
8104688
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 19, 2026
479785a
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 19, 2026
c262c79
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 20, 2026
c7aaa9a
changelog
usmansaleem Jan 20, 2026
b40071a
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 20, 2026
0e486f8
Restore changelog entry
usmansaleem Jan 21, 2026
a7f2d59
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 21, 2026
46a322b
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 21, 2026
14ce61d
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 22, 2026
9fead65
Fix merge conflicts
usmansaleem Jan 22, 2026
f0f73f0
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 23, 2026
b22d4c4
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 27, 2026
cc2cdd1
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 28, 2026
b8d81da
Use ProtocolSpec precompile registry in FourByteTracer
usmansaleem Jan 28, 2026
8f69bf9
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 28, 2026
992c3ea
simplify skip scenario
usmansaleem Jan 28, 2026
590ef1a
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 28, 2026
7c15651
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 28, 2026
7bbb749
spec tests for 4ByteTracer
usmansaleem Jan 30, 2026
38963d6
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 30, 2026
fa5929a
mark file final as it is a utility pattern class
usmansaleem Jan 30, 2026
02791dc
refactor test as per review suggestions
usmansaleem Jan 30, 2026
8407f4b
Merge remote-tracking branch 'upstream/main' into 4byte_tracer
usmansaleem Jan 30, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- Fast Sync

### Additions and Improvements
- Add support for `4byteTracer` in `debug_trace*` methods to collect function selectors from internal calls via PR [#9462][9462]. Thanks to [@JukLee0ira](https://github.com/JukLee0ira).
- Performance: Optimise EIP-196 AltBn128: EcAdd 33-128% faster, EcMul 8% faster [#9570](https://github.com/hyperledger/besu/pull/9570)
- Performance: Improved `getBlobsV2` by disabling HTTP response compression for engine API, with up to 10× throughput improvement observed for large numbers of blobs. [#9667](https://github.com/hyperledger/besu/pull/9667)
- Performance: Replace BytesTrieSet with HashSet, improves CREATE, CREATE2, SELFDESTRUCT and jumpdest analysis by up to 48% [#9641](https://github.com/hyperledger/besu/pull/9641)
Expand All @@ -56,6 +57,8 @@
- Enhance payload selection with tx count and creation time tiebreakers [#9657](https://github.com/hyperledger/besu/pull/9657)
- Fix mining beneficiary for BFT networks when set to zero address [#9679](https://github.com/hyperledger/besu/pull/9679)

[PR_9462]: https://github.com/hyperledger/besu/pull/9642

## 25.12.0

### Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.CallTracerResultConverter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.DebugTraceTransactionResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FourByteTracerResultConverter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.OpCodeLoggerTracerResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.tracing.diff.StateDiffTrace;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.tracing.diff.StateTraceGenerator;
Expand Down Expand Up @@ -85,6 +86,11 @@ public static Function<TransactionTrace, DebugTraceTransactionResult> create(
return new DebugTraceTransactionResult(
transactionTrace, new StateTraceResult(trace, diffMode));
};
case FOUR_BYTE_TRACER ->
transactionTrace -> {
var result = FourByteTracerResultConverter.convert(transactionTrace, protocolSpec);
return new DebugTraceTransactionResult(transactionTrace, result);
};
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;

import java.util.Collections;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonValue;

/**
* Represents the result format for Ethereum's 4byteTracer as specified in the Geth documentation.
*
* <p>The 4byteTracer collects function selectors (the first 4 bytes of call data) from all calls
* made during transaction execution, along with the size of the supplied call data. This is useful
* for analyzing which contract functions were invoked during a transaction.
*
* <p>The JSON output is a simple map where keys are in the format
* "0x[4-byte-selector]-[calldata-size]" and values are the number of times that combination was
* called. For example:
*
* <pre>{@code
* {
* "0x27dc297e-128": 1,
* "0x38cc4831-0": 2,
* "0x524f3889-96": 1
* }
* }</pre>
*
* @see <a
* href="https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#4byte-tracer">
* Geth 4byteTracer Documentation</a>
*/
public class FourByteTracerResult {

/**
* Map of function selector + call data size to occurrence count. Key format:
* "0x[4-byte-selector]-[calldata-size]"
*/
private final Map<String, Integer> selectorCounts;

/**
* Constructs a FourByteTracerResult with the given selector counts.
*
* @param selectorCounts map of selector-size keys to occurrence counts
*/
public FourByteTracerResult(final Map<String, Integer> selectorCounts) {
this.selectorCounts = selectorCounts != null ? selectorCounts : Collections.emptyMap();
}

/**
* Gets the map of function selectors and their occurrence counts.
*
* <p>The {@link JsonValue} annotation causes Jackson to serialize this object directly as the
* map, without wrapping it in an object. This matches Geth's output format.
*
* @return the map of selector-size keys to occurrence counts
*/
@JsonValue
public Map<String, Integer> getSelectorCounts() {
return selectorCounts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* Copyright contributors to Besu.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;

import static com.google.common.base.Preconditions.checkNotNull;

import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.calltrace.OpcodeCategory;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.evm.tracing.TraceFrame;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.tuweni.bytes.Bytes;

/**
* Converts Ethereum transaction traces into 4byte tracer format.
*
* <p>This class transforms transaction traces to collect function selectors (the first 4 bytes of
* call data) from all internal calls made during transaction execution, along with the size of the
* remaining call data (excluding the 4-byte selector). This matches Geth's 4byteTracer output
* format.
*
* <p>The converter processes only CALL-type operations that successfully enter:
*
* <ul>
* <li>CALL
* <li>CALLCODE
* <li>DELEGATECALL
* <li>STATICCALL
* </ul>
*
* <p>CREATE and CREATE2 operations are excluded, as are calls to precompiled contracts and calls
* that fail to enter.
*
* <p>For each qualifying call operation, it extracts the first 4 bytes of the input data (the
* function selector) and combines it with the size of the remaining call data (size - 4) to create
* a key in the format "0x[4-byte-selector]-[remaining-size]". The result is a map of these keys to
* their occurrence counts.
*
* @see <a href="https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/native/4byte.go">
* Geth 4byteTracer Implementation</a>
*/
public final class FourByteTracerResultConverter {

private static final int FUNCTION_SELECTOR_LENGTH = 4;

private FourByteTracerResultConverter() {
// Utility class - prevent instantiation
}

/**
* Converts a transaction trace to a 4byte tracer result.
*
* @param transactionTrace The transaction trace to convert
* @param protocolSpec Protocol Spec instance related to the transaction's block
* @return A 4byte tracer result containing function selector counts
* @throws NullPointerException if transactionTrace is null
*/
public static FourByteTracerResult convert(
final TransactionTrace transactionTrace, final ProtocolSpec protocolSpec) {
checkNotNull(
transactionTrace, "FourByteTracerResultConverter requires a non-null TransactionTrace");
checkNotNull(protocolSpec, "FourByteTracerResultConverter requires a non-null ProtocolSpec");

// Sort keys alphabetically to match Geth's JSON encoding behavior
final Map<String, Integer> selectorCounts = new TreeMap<>();

processInitialTransaction(transactionTrace, protocolSpec, selectorCounts);

// Process all trace frames for internal calls only (not the initial transaction)
if (transactionTrace.getTraceFrames() != null) {
processTraceFrames(transactionTrace.getTraceFrames(), selectorCounts);
}

return new FourByteTracerResult(selectorCounts);
}

/**
* Processes the initial transaction to extract its function selector.
*
* <p>This matches Geth's behavior where OnEnter fires for the transaction's entry into the
* contract, not just for internal calls.
*
* @param transactionTrace the transaction trace
* @param protocolSpec Protocol Spec instance to obtain precompile registry
* @param selectorCounts the map to update with selector counts
*/
private static void processInitialTransaction(
final TransactionTrace transactionTrace,
final ProtocolSpec protocolSpec,
final Map<String, Integer> selectorCounts) {

final Transaction tx = transactionTrace.getTransaction();

// Skip scenarios
if (tx.isContractCreation()
|| tx.getTo().isEmpty()
|| isTargetPrecompiledContract(protocolSpec, tx.getTo().get())) {
return;
}

final Bytes inputData = tx.getPayload();

// Only process if we have at least 4 bytes for the function selector
if (inputData != null && inputData.size() >= FUNCTION_SELECTOR_LENGTH) {
final String key = createKey(inputData);
selectorCounts.merge(key, 1, Integer::sum);
}
}

private static boolean isTargetPrecompiledContract(
final ProtocolSpec protocolSpec, final Address toAddress) {
return protocolSpec.getPrecompileContractRegistry().get(toAddress) != null;
}

/**
* Processes all trace frames to extract function selectors from CALL operations.
*
* <p>This method only processes CALL, CALLCODE, DELEGATECALL, and STATICCALL operations that
* successfully enter a new execution scope. CREATE and CREATE2 operations are ignored to match
* Geth's behavior.
*
* @param frames the list of trace frames to process
* @param selectorCounts the map to update with selector counts
*/
private static void processTraceFrames(
final List<TraceFrame> frames, final Map<String, Integer> selectorCounts) {

for (int i = 0; i < frames.size(); i++) {
final TraceFrame frame = frames.get(i);
final String opcode = frame.getOpcode();

// Only process CALL-type operations (not CREATE/CREATE2)
if (OpcodeCategory.isCallOp(opcode)) {
final TraceFrame nextTrace = (i < frames.size() - 1) ? frames.get(i + 1) : null;
processCall(frame, nextTrace, selectorCounts);
}
}
}

/**
* Processes a single CALL operation to extract its function selector.
*
* <p>This method:
*
* <ul>
* <li>Checks if the call actually entered (depth increased in next frame)
* <li>Skips calls to precompiled contracts
* <li>Extracts the input data from the entered call
* <li>Skips calls with less than 4 bytes of input (no function selector)
* <li>Creates a key in the format "0x[selector]-[size-4]" and increments its count
* </ul>
*
* <p>This matches Geth's OnEnter hook behavior which only fires when a call successfully enters a
* new execution scope.
*
* @param frame the current trace frame (the CALL instruction)
* @param nextTrace the next trace frame (first frame inside the call, may be null)
* @param selectorCounts the map to update with selector counts
*/
private static void processCall(
final TraceFrame frame,
final TraceFrame nextTrace,
final Map<String, Integer> selectorCounts) {

// Skip precompiled contracts
if (frame.isPrecompile()) {
return;
}

// Check if call entered (depth increased) - matches Geth's OnEnter behavior
final boolean calleeEntered = nextTrace != null && nextTrace.getDepth() > frame.getDepth();
if (!calleeEntered) {
return;
}

// Get the input data from the entered call (nextTrace has the actual input data)
final Bytes inputData = nextTrace.getInputData();

// Only process if we have at least 4 bytes for the function selector
if (inputData != null && inputData.size() >= FUNCTION_SELECTOR_LENGTH) {
final String key = createKey(inputData);
selectorCounts.merge(key, 1, Integer::sum);
}
}

/**
* Creates a key in the format "0x[4-byte-selector]-[size-4]" from the input data.
*
* <p>The size portion represents the length of the input data minus the 4-byte selector, matching
* Geth's implementation where the size is calculated as `len(input) - 4`.
*
* @param inputData the input data containing the function selector
* @return the formatted key (e.g., "0x27dc297e-128")
*/
private static String createKey(final Bytes inputData) {
final Bytes selector = inputData.slice(0, FUNCTION_SELECTOR_LENGTH);
final int remainingSize = inputData.size() - FUNCTION_SELECTOR_LENGTH;
return selector.toHexString() + "-" + remainingSize;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public static Object[][] specs() {
new String[] {
"debug-geth/specs/prestate-tracer/diff-mode-true",
"debug-geth/specs/prestate-tracer/diff-mode-false",
"debug-geth/specs/call-tracer"
"debug-geth/specs/call-tracer",
"debug-geth/specs/4byte-tracer"
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.DebugTraceTransactionResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FourByteTracerResult;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.OpCodeLoggerTracerResult;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.debug.TraceOptions;
import org.hyperledger.besu.ethereum.debug.TracerType;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult;
import org.hyperledger.besu.evm.precompile.PrecompileContractRegistry;

import java.util.Collections;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -62,6 +64,11 @@ void setUp() {
mockResult = mock(TransactionProcessingResult.class);
mockProtocolSpec = mock(ProtocolSpec.class);

// Setup PrecompileContractRegistry for FourByteTracer
PrecompileContractRegistry mockRegistry = mock(PrecompileContractRegistry.class);
when(mockProtocolSpec.getPrecompileContractRegistry()).thenReturn(mockRegistry);
when(mockRegistry.get(org.mockito.ArgumentMatchers.any(Address.class))).thenReturn(null);

// Set up transaction hash chain
when(mockTransactionTrace.getTransaction()).thenReturn(mockTransaction);
when(mockTransaction.getHash()).thenReturn(mockHash);
Expand Down Expand Up @@ -97,6 +104,24 @@ void shouldCreateFunctionForOpcodeTracer() {
assertThat(result.getResult()).isInstanceOf(OpCodeLoggerTracerResult.class);
}

@Test
@DisplayName("should create function for FOUR_BYTE_TRACER that returns FourByteTracerResult")
void shouldCreateFunctionForFourByteTracer() {
// Given
TracerType tracerType = TracerType.FOUR_BYTE_TRACER;
TraceOptions traceOptions = new TraceOptions(tracerType, null, null);
Function<TransactionTrace, DebugTraceTransactionResult> function =
DebugTraceTransactionStepFactory.create(traceOptions, mockProtocolSpec);

// When
DebugTraceTransactionResult result = function.apply(mockTransactionTrace);

// Then
assertThat(result).isNotNull();
assertThat(result.getTxHash()).isEqualTo(EXPECTED_HASH);
assertThat(result.getResult()).isInstanceOf(FourByteTracerResult.class);
}

@ParameterizedTest
@EnumSource(
value = TracerType.class,
Expand Down
Loading