Skip to content

Commit e0cfa80

Browse files
committed
4byteTracer implementation for debug_trace json-rpc
Signed-off-by: JukLee0ira <meebookMonkey@163.com>
1 parent 828226b commit e0cfa80

File tree

5 files changed

+292
-1
lines changed

5 files changed

+292
-1
lines changed

ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/DebugTraceTransactionIntegrationTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
2929
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
3030
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.CallTracerResult;
31+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FourByteTracerResult;
3132
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.OpCodeLoggerTracerResult;
3233
import org.hyperledger.besu.plugin.services.rpc.RpcResponseType;
3334
import org.hyperledger.besu.testutil.BlockTestUtil;
@@ -120,6 +121,33 @@ public void debugTraceTransactionCallTracerSuccessTest() {
120121
assertThat(result.getInput()).isEqualTo("0x9dc2c8f5");
121122
}
122123

124+
@Test
125+
public void debugTraceTransactionFourByteTracerSuccessTest() {
126+
final Map<String, String> map = Map.of("tracer", "4byteTracer");
127+
final Hash trxHash =
128+
Hash.fromHexString("0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310");
129+
final Object[] params = new Object[] {trxHash, map};
130+
final JsonRpcRequestContext request =
131+
new JsonRpcRequestContext(new JsonRpcRequest("2.0", DEBUG_TRACE_TRANSACTION, params));
132+
133+
final JsonRpcResponse response = method.response(request);
134+
assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS);
135+
assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class);
136+
137+
final FourByteTracerResult result =
138+
(FourByteTracerResult) ((JsonRpcSuccessResponse) response).getResult();
139+
140+
// Verify that the result contains function selector mappings
141+
final Map<String, Integer> selectorCounts = result.getResult();
142+
assertThat(selectorCounts).isNotNull();
143+
assertThat(selectorCounts).isNotEmpty();
144+
145+
// Verify that the function selector from the transaction input is captured
146+
// The transaction input 0x9dc2c8f5 should result in selector 0x9dc2c8f5 with parameter data size 0
147+
assertThat(selectorCounts).containsKey("0x9dc2c8f5-0");
148+
assertThat(selectorCounts.get("0x9dc2c8f5-0")).isEqualTo(1);
149+
}
150+
123151
@Test
124152
public void invalidTracerTypeErrorTest() {
125153
final Map<String, String> map = Map.of("tracer", "invalidTracerType");

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugTraceTransactionStepFactory.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
1818
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.CallTracerResultConverter;
1919
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.DebugTraceTransactionResult;
20+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FourByteTracerResultConverter;
2021
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.OpCodeLoggerTracerResult;
2122
import org.hyperledger.besu.ethereum.debug.TracerType;
2223

@@ -76,6 +77,15 @@ public static Function<TransactionTrace, DebugTraceTransactionResult> create(
7677
var result = new UnimplementedTracerResult();
7778
return new DebugTraceTransactionResult(transactionTrace, result);
7879
};
80+
case FOUR_BYTE_TRACER ->
81+
transactionTrace -> {
82+
if (enableExtraTracers) {
83+
var result = FourByteTracerResultConverter.convert(transactionTrace);
84+
return new DebugTraceTransactionResult(transactionTrace, result);
85+
}
86+
return new DebugTraceTransactionResult(
87+
transactionTrace, new UnimplementedTracerResult());
88+
};
7989
};
8090
}
8191

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;
16+
17+
import com.fasterxml.jackson.annotation.JsonGetter;
18+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
19+
import java.util.Map;
20+
21+
/**
22+
* Result returned by the 4byteTracer.
23+
*
24+
* <p>The 4byteTracer collects the function selectors of every function executed in the lifetime of
25+
* a transaction, along with the size of the supplied parameter data (excluding the 4-byte selector).
26+
* The result is a map where the keys are SELECTOR-PARAMETERDATASIZE and the values are number of
27+
* occurrences of this key.
28+
*
29+
* The size represents len(input)-4, which is the size of the
30+
* parameter data excluding the function selector.
31+
*
32+
* <p>For example:
33+
* <pre>
34+
* {
35+
* "0x27dc297e-128": 1,
36+
* "0x38cc4831-0": 2,
37+
* "0x524f3889-96": 1,
38+
* "0xadf59f99-288": 1,
39+
* "0xc281d19e-0": 1
40+
* }
41+
* </pre>
42+
*/
43+
@JsonPropertyOrder
44+
public class FourByteTracerResult implements JsonRpcResult {
45+
46+
private final Map<String, Integer> result;
47+
48+
/**
49+
* Creates a new FourByteTracerResult with the given result map.
50+
*
51+
* @param result a map of function selector-calldata size to occurrence count
52+
*/
53+
public FourByteTracerResult(final Map<String, Integer> result) {
54+
this.result = result;
55+
}
56+
57+
/**
58+
* Returns the result map containing function selectors and their occurrence counts.
59+
*
60+
* @return a map where keys are "selector-calldatasize" and values are occurrence counts
61+
*/
62+
@JsonGetter
63+
public Map<String, Integer> getResult() {
64+
return result;
65+
}
66+
67+
@Override
68+
public boolean equals(final Object o) {
69+
if (this == o) return true;
70+
if (o == null || getClass() != o.getClass()) return false;
71+
final FourByteTracerResult that = (FourByteTracerResult) o;
72+
return result != null ? result.equals(that.result) : that.result == null;
73+
}
74+
75+
@Override
76+
public int hashCode() {
77+
return result != null ? result.hashCode() : 0;
78+
}
79+
80+
@Override
81+
public String toString() {
82+
return "FourByteTracerResult{" + "result=" + result + '}';
83+
}
84+
}
85+
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;
16+
17+
import static com.google.common.base.Preconditions.checkNotNull;
18+
19+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.TransactionTrace;
20+
import org.hyperledger.besu.ethereum.core.Transaction;
21+
import org.hyperledger.besu.evm.tracing.TraceFrame;
22+
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
import org.apache.tuweni.bytes.Bytes;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Converts Ethereum transaction traces into 4byte tracer format.
33+
*
34+
* <p>The 4byteTracer collects the function selectors of every function executed in the lifetime of
35+
* a transaction, along with the size of the supplied call data. The result is a map where the keys
36+
* are SELECTOR-PARAMETERDATASIZE and the values are number of occurrences of this key.
37+
*
38+
* <p>Function selectors are the first 4 bytes of the Keccak-256 hash of function signatures, which
39+
* are used to identify which function to call in Solidity contracts.
40+
*/
41+
public class FourByteTracerResultConverter {
42+
private static final Logger LOG = LoggerFactory.getLogger(FourByteTracerResultConverter.class);
43+
44+
45+
/**
46+
* Converts a transaction trace to a 4byte tracer result.
47+
*
48+
* @param transactionTrace The transaction trace to convert
49+
* @return A 4byte tracer result containing function selectors and their occurrence counts
50+
* @throws NullPointerException if transactionTrace or its components are null
51+
*/
52+
public static FourByteTracerResult convert(final TransactionTrace transactionTrace) {
53+
checkNotNull(
54+
transactionTrace, "FourByteTracerResultConverter requires a non-null TransactionTrace");
55+
checkNotNull(
56+
transactionTrace.getTransaction(),
57+
"FourByteTracerResultConverter requires non-null Transaction");
58+
checkNotNull(
59+
transactionTrace.getResult(),
60+
"FourByteTracerResultConverter requires non-null Result");
61+
62+
final Map<String, Integer> selectorCounts = new HashMap<>();
63+
final Transaction transaction = transactionTrace.getTransaction();
64+
final List<TraceFrame> traceFrames = transactionTrace.getTraceFrames();
65+
66+
// Process the initial transaction call data only for message-call transactions
67+
if (transaction.getTo().isPresent()) {
68+
processCallData(transaction.getPayload(), selectorCounts);
69+
}
70+
71+
// Process all trace frames for additional function calls
72+
if (traceFrames != null) {
73+
LOG.trace("Processing {} trace frames for 4byte tracer", traceFrames.size());
74+
for (final TraceFrame frame : traceFrames) {
75+
if (shouldProcessFrame(frame)) {
76+
final Bytes inputData = frame.getInputData();
77+
if (inputData != null) {
78+
processCallData(inputData, selectorCounts);
79+
}
80+
}
81+
}
82+
}
83+
84+
LOG.trace("4byte tracer found {} unique function selectors", selectorCounts.size());
85+
return new FourByteTracerResult(selectorCounts);
86+
}
87+
88+
/**
89+
* Processes call data to extract function selector and update counts.
90+
*
91+
* Stores the 4-byte function selector along with the size
92+
* of the call data minus the 4-byte selector (i.e., the actual parameter data size).
93+
*
94+
* @param callData The call data to process
95+
* @param selectorCounts The map to update with selector counts
96+
*/
97+
private static void processCallData(final Bytes callData, final Map<String, Integer> selectorCounts) {
98+
if (callData == null || callData.size() < 4) {
99+
// Not enough data for a function selector
100+
return;
101+
}
102+
103+
// Extract the first 4 bytes as the function selector
104+
final Bytes selector = callData.slice(0, 4);
105+
final String selectorHex = selector.toHexString();
106+
107+
// Use len(input)-4 (parameter data size, excluding selector)
108+
final int parameterDataSize = callData.size() - 4;
109+
110+
// Create the key in the format "selector-parameterdatasize"
111+
final String key = selectorHex + "-" + parameterDataSize;
112+
113+
// Update the count
114+
selectorCounts.put(key, selectorCounts.getOrDefault(key, 0) + 1);
115+
116+
LOG.trace("4byte tracer: selector {} with parameter data size {} (key: {})",
117+
selectorHex, parameterDataSize, key);
118+
}
119+
120+
/**
121+
* Determines if a trace frame should be processed for 4byte analysis.
122+
*
123+
* Only process CALL, CALLCODE, DELEGATECALL, and STATICCALL operations,
124+
* excluding CREATE/CREATE2 and precompiled contracts.
125+
*
126+
* @param frame The trace frame to check
127+
* @return true if the frame should be processed, false otherwise
128+
*/
129+
private static boolean shouldProcessFrame(final TraceFrame frame) {
130+
final String opcode = frame.getOpcode();
131+
132+
// Only process specific call operations
133+
if (!isRelevantCallOperation(opcode)) {
134+
return false;
135+
}
136+
137+
// Skip precompiled contracts (portable across forks/chains)
138+
if (frame.isPrecompile()) {
139+
LOG.trace("Skipping precompiled contract call");
140+
return false;
141+
}
142+
143+
// Ensure we have enough input data for a function selector
144+
final Bytes inputData = frame.getInputData();
145+
return inputData != null && inputData.size() >= 4;
146+
}
147+
148+
/**
149+
* Checks if the given opcode represents a relevant call operation for 4byte tracing.
150+
*
151+
* Only CALL, CALLCODE, DELEGATECALL, and STATICCALL
152+
* are relevant. CREATE and CREATE2 are explicitly excluded.
153+
*
154+
* @param opcode The opcode to check
155+
* @return true if the opcode is a relevant call operation, false otherwise
156+
*/
157+
private static boolean isRelevantCallOperation(final String opcode) {
158+
return "CALL".equals(opcode) ||
159+
"CALLCODE".equals(opcode) ||
160+
"DELEGATECALL".equals(opcode) ||
161+
"STATICCALL".equals(opcode);
162+
// Note: CREATE and CREATE2 are intentionally excluded
163+
}
164+
}
165+

ethereum/core/src/main/java/org/hyperledger/besu/ethereum/debug/TracerType.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public enum TracerType {
3131
FLAT_CALL_TRACER("flatCallTracer", "Flat Call Tracer"),
3232

3333
/** Prestate tracer that captures account states before transaction execution */
34-
PRESTATE_TRACER("prestateTracer", "Prestate Tracer");
34+
PRESTATE_TRACER("prestateTracer", "Prestate Tracer"),
35+
36+
/** 4byte tracer that collects function selectors and call data sizes */
37+
FOUR_BYTE_TRACER("4byteTracer", "4byte Tracer");
3538

3639
private final String value;
3740
private final String displayName;

0 commit comments

Comments
 (0)