Skip to content

Commit 40ce179

Browse files
committed
implement trace filtering by opcode in debug_* RPCs
Signed-off-by: Luis Pinto <[email protected]>
1 parent b87ccb2 commit 40ce179

File tree

7 files changed

+211
-4
lines changed

7 files changed

+211
-4
lines changed

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.nio.charset.StandardCharsets;
3333
import java.nio.file.Files;
3434
import java.nio.file.Path;
35+
import java.util.Arrays;
3536
import java.util.List;
3637
import java.util.Map;
3738

@@ -253,6 +254,84 @@ public void defaultFieldsAssertStorage() throws IOException {
253254
assertThat(json).noneMatch(node -> node.has("storage"));
254255
}
255256

257+
@Test
258+
public void defaultFieldsAssertOpcodesWithTxHash() throws IOException {
259+
final Map<String, String> map =
260+
Map.of("txHash", "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed");
261+
final Hash blockHash =
262+
Hash.fromHexString("0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9");
263+
final Object[] params = new Object[] {blockHash, map};
264+
final JsonRpcRequestContext request =
265+
new JsonRpcRequestContext(new JsonRpcRequest("2.0", RPC_ENDPOINT, params));
266+
267+
final JsonRpcResponse response = method.response(request);
268+
assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS);
269+
List<?> files = (List<?>) ((JsonRpcSuccessResponse) response).getResult();
270+
271+
ObjectMapper jsonMapper = new ObjectMapper();
272+
List<JsonNode> json =
273+
Files.readAllLines(Path.of((String) files.getFirst())).stream()
274+
.map(
275+
line -> {
276+
try {
277+
return jsonMapper.readTree(line);
278+
} catch (JsonProcessingException e) {
279+
Assert.fail("encountered invalid json from RPC response");
280+
throw new RuntimeException(e);
281+
}
282+
})
283+
.toList();
284+
285+
assertThat(json)
286+
.allMatch(
287+
node ->
288+
node.equals(json.getLast())
289+
|| "PUSH1".equals(node.get("opName").asText())
290+
|| "JUMPDEST".equals(node.get("opName").asText())
291+
|| "DUP1".equals(node.get("opName").asText())
292+
|| "RETURN".equals(node.get("opName").asText())
293+
|| "PUSH2".equals(node.get("opName").asText())
294+
|| "CODECOPY".equals(node.get("opName").asText()));
295+
}
296+
297+
@Test
298+
public void defaultFieldsAssertOpcodes() throws IOException {
299+
final Hash blockHash =
300+
Hash.fromHexString("0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9");
301+
final Object[] params = new Object[] {blockHash};
302+
final JsonRpcRequestContext request =
303+
new JsonRpcRequestContext(new JsonRpcRequest("2.0", RPC_ENDPOINT, params));
304+
305+
final JsonRpcResponse response = method.response(request);
306+
assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS);
307+
List<?> files = (List<?>) ((JsonRpcSuccessResponse) response).getResult();
308+
309+
ObjectMapper jsonMapper = new ObjectMapper();
310+
List<JsonNode> json =
311+
Files.readAllLines(Path.of((String) files.getFirst())).stream()
312+
.map(
313+
line -> {
314+
try {
315+
return jsonMapper.readTree(line);
316+
} catch (JsonProcessingException e) {
317+
Assert.fail("encountered invalid json from RPC response");
318+
throw new RuntimeException(e);
319+
}
320+
})
321+
.toList();
322+
323+
assertThat(json)
324+
.allMatch(
325+
node ->
326+
node.equals(json.getLast())
327+
|| "PUSH1".equals(node.get("opName").asText())
328+
|| "JUMPDEST".equals(node.get("opName").asText())
329+
|| "DUP1".equals(node.get("opName").asText())
330+
|| "RETURN".equals(node.get("opName").asText())
331+
|| "PUSH2".equals(node.get("opName").asText())
332+
|| "CODECOPY".equals(node.get("opName").asText()));
333+
}
334+
256335
@Test
257336
public void disableMemory() throws IOException {
258337
final Map<String, Boolean> map = Map.of("disableMemory", true);
@@ -342,4 +421,39 @@ public void enableStorage() throws IOException {
342421

343422
assertThat(json).anyMatch(node -> node.has("storage"));
344423
}
424+
425+
@Test
426+
public void traceOpcodes() throws IOException {
427+
final Map<String, List<String>> map = Map.of("opcodes", Arrays.asList("PUSH2", "CODECOPY"));
428+
final Hash blockHash =
429+
Hash.fromHexString("0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9");
430+
final Object[] params = new Object[] {blockHash, map};
431+
final JsonRpcRequestContext request =
432+
new JsonRpcRequestContext(new JsonRpcRequest("2.0", RPC_ENDPOINT, params));
433+
434+
final JsonRpcResponse response = method.response(request);
435+
assertThat(response.getType()).isEqualTo(RpcResponseType.SUCCESS);
436+
List<?> files = (List<?>) ((JsonRpcSuccessResponse) response).getResult();
437+
438+
ObjectMapper jsonMapper = new ObjectMapper();
439+
List<JsonNode> json =
440+
Files.readAllLines(Path.of((String) files.getFirst())).stream()
441+
.map(
442+
line -> {
443+
try {
444+
return jsonMapper.readTree(line);
445+
} catch (JsonProcessingException e) {
446+
Assert.fail("encountered invalid json from RPC response");
447+
throw new RuntimeException(e);
448+
}
449+
})
450+
.toList();
451+
452+
assertThat(json)
453+
.allMatch(
454+
node ->
455+
node.equals(json.getLast())
456+
|| "PUSH2".equals(node.get("opName").asText())
457+
|| "CODECOPY".equals(node.get("opName").asText()));
458+
}
345459
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@
2929
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
3030
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.CallTracerResult;
3131
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.OpCodeLoggerTracerResult;
32+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.StructLog;
3233
import org.hyperledger.besu.plugin.services.rpc.RpcResponseType;
3334
import org.hyperledger.besu.testutil.BlockTestUtil;
3435

36+
import java.io.IOException;
3537
import java.nio.charset.StandardCharsets;
38+
import java.util.Arrays;
39+
import java.util.List;
3640
import java.util.Map;
3741

42+
import com.fasterxml.jackson.databind.JsonNode;
43+
import com.fasterxml.jackson.databind.ObjectMapper;
3844
import com.google.common.io.Resources;
3945
import org.junit.jupiter.api.BeforeAll;
4046
import org.junit.jupiter.api.BeforeEach;
@@ -101,7 +107,6 @@ public void debugTraceTransactionMissingTest() {
101107

102108
@Test
103109
public void debugTraceTransactionCallTracerSuccessTest() {
104-
105110
final Map<String, String> map = Map.of("tracer", "callTracer");
106111
final Hash trxHash =
107112
Hash.fromHexString("0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310");
@@ -133,4 +138,27 @@ public void invalidTracerTypeErrorTest() {
133138
.isThrownBy(() -> method.response(request))
134139
.withMessage("Invalid Tracer Type: invalidTracerType.");
135140
}
141+
142+
@Test
143+
public void debugTraceTransactionSpecificOpcodes() {
144+
final Map<String, List<String>> map = Map.of("opcodes", Arrays.asList("EQ", "DIV"));
145+
final Hash trxHash =
146+
Hash.fromHexString("0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310");
147+
final Object[] params = new Object[] {trxHash, map};
148+
final JsonRpcRequestContext request =
149+
new JsonRpcRequestContext(new JsonRpcRequest("2.0", DEBUG_TRACE_TRANSACTION, params));
150+
final Object result = ((JsonRpcSuccessResponse) method.response(request)).getResult();
151+
152+
List<StructLog> logs = ((OpCodeLoggerTracerResult) result).getStructLogs();
153+
154+
ObjectMapper jsonMapper = new ObjectMapper();
155+
List<JsonNode> json = logs.stream().<JsonNode>map(jsonMapper::valueToTree).toList();
156+
157+
assertThat(json)
158+
.allMatch(
159+
node ->
160+
node.equals(json.getLast())
161+
|| "EQ".equals(node.get("op").asText())
162+
|| "DIV".equals(node.get("op").asText()));
163+
}
136164
}

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder;
2020
import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder.OpCodeTracerConfig;
2121

22+
import java.util.Collections;
2223
import java.util.LinkedHashMap;
24+
import java.util.Set;
2325
import javax.annotation.Nullable;
2426

2527
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -77,6 +79,12 @@ default boolean disableStack() {
7779
@SuppressWarnings("NonApiType")
7880
LinkedHashMap<String, Object> tracerConfig();
7981

82+
@JsonProperty("opcodes")
83+
@Value.Default
84+
default Set<String> opcodes() {
85+
return Collections.emptySet();
86+
}
87+
8088
/**
8189
* Convert JSON-RPC parameters to a {@link TraceOptions} object.
8290
*
@@ -88,6 +96,7 @@ default TraceOptions traceOptions() {
8896
.traceStorage(!disableStorage())
8997
.traceMemory(!disableMemory())
9098
.traceStack(!disableStack())
99+
.traceOpcodes(opcodes())
91100
.build();
92101

93102
// Convert string tracer to TracerType enum, handling null case

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/processor/TransactionTracer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.nio.file.Files;
4343
import java.nio.file.Path;
4444
import java.util.ArrayList;
45+
import java.util.Collections;
4546
import java.util.List;
4647
import java.util.Optional;
4748
import java.util.concurrent.TimeUnit;
@@ -97,7 +98,7 @@ public List<String> traceTransactionToFile(
9798
.map(TransactionTraceParams::getTransactionHash)
9899
.map(Hash::fromHexString);
99100
final OpCodeTracerConfig opCodeTracerConfig =
100-
OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT)
101+
OpCodeTracerConfigBuilder.create()
101102
.traceMemory(
102103
transactionTraceParams
103104
.map(TransactionTraceParams::disableMemoryNullable)
@@ -114,6 +115,10 @@ public List<String> traceTransactionToFile(
114115
.map(Boolean.FALSE::equals)
115116
.orElse(false))
116117
.traceReturnData(true)
118+
.traceOpcodes(
119+
transactionTraceParams
120+
.map(TransactionTraceParams::opcodes)
121+
.orElse(Collections.emptySet()))
117122
.eip3155Strict(true)
118123
.build();
119124

ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import java.util.ArrayList;
3232
import java.util.List;
33+
import java.util.Locale;
3334
import java.util.Map;
3435
import java.util.Optional;
3536
import java.util.OptionalLong;
@@ -72,6 +73,11 @@ public DebugOperationTracer(final OpCodeTracerConfig options, final boolean reco
7273

7374
@Override
7475
public void tracePreExecution(final MessageFrame frame) {
76+
final Operation currentOperation = frame.getCurrentOperation();
77+
if (!options.traceOpcodes().isEmpty()
78+
&& !options.traceOpcodes().contains(currentOperation.getName().toLowerCase(Locale.ROOT))) {
79+
return;
80+
}
7581
preExecutionStack = captureStack(frame);
7682
gasRemaining = frame.getRemainingGas();
7783
if (lastFrame != null && frame.getDepth() > lastFrame.getDepth())
@@ -85,6 +91,10 @@ public void tracePreExecution(final MessageFrame frame) {
8591
public void tracePostExecution(final MessageFrame frame, final OperationResult operationResult) {
8692
final Operation currentOperation = frame.getCurrentOperation();
8793
final String opcode = currentOperation.getName();
94+
if (!options.traceOpcodes().isEmpty()
95+
&& !options.traceOpcodes().contains(opcode.toLowerCase(Locale.ROOT))) {
96+
return;
97+
}
8898
final int opcodeNumber = (opcode != null) ? currentOperation.getOpcode() : Integer.MAX_VALUE;
8999
final WorldUpdater worldUpdater = frame.getWorldUpdater();
90100
final Bytes outputData = frame.getOutputData();

evm/src/main/java/org/hyperledger/besu/evm/tracing/OpCodeTracerConfigBuilder.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
*/
1515
package org.hyperledger.besu.evm.tracing;
1616

17+
import java.util.Collections;
1718
import java.util.Objects;
19+
import java.util.Set;
20+
import java.util.stream.Collectors;
1821

1922
/** Configuration for the default struct/opcode tracer. */
2023
public final class OpCodeTracerConfigBuilder {
2124
private boolean traceStorage;
2225
private boolean traceMemory;
2326
private boolean traceStack;
2427
private boolean traceReturnData;
28+
private Set<String> traceOpcodes;
2529
private boolean eip3155Strict;
2630

2731
/**
@@ -51,6 +55,7 @@ private OpCodeTracerConfigBuilder(final OpCodeTracerConfig opCodeTracerConfig) {
5155
this.traceMemory = opCodeTracerConfig.traceMemory();
5256
this.traceStack = opCodeTracerConfig.traceStack();
5357
this.traceReturnData = opCodeTracerConfig.traceReturnData();
58+
this.traceOpcodes = opCodeTracerConfig.traceOpcodes();
5459
this.eip3155Strict = opCodeTracerConfig.eip3155Strict();
5560
}
5661

@@ -98,6 +103,17 @@ public OpCodeTracerConfigBuilder traceReturnData(final boolean enable) {
98103
return this;
99104
}
100105

106+
/**
107+
* Sets the list of opcodes to trace
108+
*
109+
* @param traceOpcodes list of opcodes to trace
110+
* @return the current builder
111+
*/
112+
public OpCodeTracerConfigBuilder traceOpcodes(final Set<String> traceOpcodes) {
113+
this.traceOpcodes = traceOpcodes.stream().map(String::toLowerCase).collect(Collectors.toSet());
114+
return this;
115+
}
116+
101117
/**
102118
* Set eip3155Strict flag.
103119
*
@@ -115,7 +131,8 @@ public OpCodeTracerConfigBuilder eip3155Strict(final boolean enable) {
115131
* @return the config
116132
*/
117133
public OpCodeTracerConfig build() {
118-
return new Config(traceStorage, traceMemory, traceStack, traceReturnData, eip3155Strict);
134+
return new Config(
135+
traceStorage, traceMemory, traceStack, traceReturnData, traceOpcodes, eip3155Strict);
119136
}
120137

121138
/**
@@ -126,7 +143,8 @@ public OpCodeTracerConfig build() {
126143
*/
127144
public sealed interface OpCodeTracerConfig permits Config {
128145
/** static default OpcodeTracerConfig which can be accessed externally */
129-
OpCodeTracerConfig DEFAULT = new Config(true, false, true, false, false);
146+
OpCodeTracerConfig DEFAULT =
147+
new Config(true, false, true, false, Collections.emptySet(), false);
130148

131149
/**
132150
* Check if tracing of storage is enabled.
@@ -156,6 +174,14 @@ public sealed interface OpCodeTracerConfig permits Config {
156174
*/
157175
boolean traceReturnData();
158176

177+
/**
178+
* List of opcodes that the tracer should exclusively trace for. If empty, all opcodes are
179+
* traced.
180+
*
181+
* @return list of opcodes to trace.
182+
*/
183+
Set<String> traceOpcodes();
184+
159185
/**
160186
* Check if tracing in eip3155 mode is enabled.
161187
*
@@ -169,6 +195,7 @@ private record Config(
169195
boolean traceMemory,
170196
boolean traceStack,
171197
boolean traceReturnData,
198+
Set<String> traceOpcodes,
172199
boolean eip3155Strict)
173200
implements OpCodeTracerConfig {}
174201
}

0 commit comments

Comments
 (0)