diff --git a/CHANGELOG.md b/CHANGELOG.md index ab941605948..9b8bc52f380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ - Enforce EIP-7928 Block Access List item budget per transaction in both processing and mining [#10250](https://github.com/besu-eth/besu/pull/10250) - Include `slotNumber` in `payloadIdentifier` generation [#10242](https://github.com/besu-eth/besu/pull/10242) - Change Block Access List index encoding to `uint32` [#10279](https://github.com/besu-eth/besu/pull/10279) +- Add `limit` support to debug opcode tracing to stop capture after N EVM steps (`0` keeps the previous unlimited behavior) and reject negative values at the RPC parameter boundary [#10173](https://github.com/besu-eth/besu/pull/10173) ### Plugin API - Publish Guava as an API dependency from `plugin-api` [#10248](https://github.com/besu-eth/besu/pull/10248) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java index 5ebad5d8c47..3f5bb4ec6ac 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParams.java @@ -69,6 +69,9 @@ default boolean disableStack() { return Boolean.TRUE.equals(disableStackNullable()); } + @JsonProperty(value = "limit") + @Nullable Integer limit(); + @JsonProperty("tracer") @JsonInclude(JsonInclude.Include.NON_NULL) @Nullable String tracer(); @@ -93,6 +96,13 @@ default Set opcodes() { @JsonInclude(JsonInclude.Include.NON_NULL) StateOverrideMap stateOverrides(); + @Value.Check + default void validate() { + if (limit() != null && limit() < 0) { + throw new IllegalArgumentException("limit must be >= 0, got: " + limit()); + } + } + /** * Convert JSON-RPC parameters to a {@link TraceOptions} object. * @@ -122,6 +132,9 @@ default TraceOptions traceOptions() { if (disableStackNullable() != null) { builder.traceStack(!disableStack()); } + if (limit() != null) { + builder.limit(limit()); + } var opCodeTracerConfig = builder.traceOpcodes(opcodes()).build(); return new TraceOptions(tracerType, opCodeTracerConfig, tracerConfig(), stateOverrides()); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParamsTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParamsTest.java index 2775789627b..eaee3ce96c9 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParamsTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/parameters/TransactionTraceParamsTest.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.hyperledger.besu.ethereum.debug.TraceOptions; import org.hyperledger.besu.evm.tracing.OpCodeTracerConfigBuilder.OpCodeTracerConfig; @@ -131,4 +132,10 @@ public void nonOpcodeTracerShouldRespectExplicitEnableMemoryFalse() throws Excep .describedAs("explicit enableMemory=false should be respected for callTracer") .isFalse(); } + + @Test + public void negativeLimitShouldFailDuringDeserialization() { + assertThatThrownBy(() -> MAPPER.readValue("{\"limit\": -1}", TransactionTraceParams.class)) + .hasMessageContaining("limit must be >= 0, got: -1"); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java index 04f1952645b..c35e8fc2a17 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracer.java @@ -43,6 +43,8 @@ public class DebugOperationTracer extends AbstractDebugOperationTracer { private TraceFrame lastFrame; private Bytes inputData; + private int stepCount; + private boolean limitReached; /** * Creates the operation tracer. @@ -57,6 +59,12 @@ public DebugOperationTracer(final OpCodeTracerConfig options, final boolean reco @Override protected void capturePreExecutionState(final MessageFrame frame) { + if (options.limit() > 0 && stepCount >= options.limit()) { + limitReached = true; + return; + } + limitReached = false; + stepCount++; if (lastFrame != null && frame.getDepth() > lastFrame.getDepth()) inputData = frame.getInputData().copy(); else inputData = frame.getInputData(); @@ -64,9 +72,6 @@ protected void capturePreExecutionState(final MessageFrame frame) { @Override public void tracePostExecution(final MessageFrame frame, final OperationResult operationResult) { - if (!traceOpcode) { - return; - } final Operation currentOperation = frame.getCurrentOperation(); final String opcode = currentOperation.getName(); final int opcodeNumber = (opcode != null) ? currentOperation.getOpcode() : Integer.MAX_VALUE; @@ -89,6 +94,9 @@ public void tracePostExecution(final MessageFrame frame, final OperationResult o TraceFrame.from(lastTraceFrame).setGasRemainingPostExecution(gasRemaining).build(); traceFrames.add(updatedLast); } + if (limitReached || !traceOpcode) { + return; + } final Optional> storage = captureStorage(frame); final Optional> maybeRefunds = @@ -275,6 +283,8 @@ public List getTraceFrames() { public void reset() { traceFrames = new ArrayList<>(); lastFrame = null; + stepCount = 0; + limitReached = false; } public List copyTraceFrames() { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java index 5df6f63c993..024744abbbd 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/vm/DebugOperationTracerTest.java @@ -417,6 +417,64 @@ void shouldHandleCallScenarioAndDepthChange() { .isEqualTo(WORD_1); } + @Test + void shouldCaptureAtMostLimitFrames() { + final OpCodeTracerConfig config = + OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT) + .traceStorage(false) + .traceMemory(false) + .traceStack(false) + .limit(2) + .build(); + final DebugOperationTracer tracer = new DebugOperationTracer(config, false); + final MessageFrame frame = validMessageFrame(); + + for (int i = 0; i < 5; i++) { + traceFrame(frame, tracer, anOperation); + } + + assertThat(tracer.getTraceFrames()).hasSize(2); + } + + @Test + void shouldBackfillGasRemainingPostExecutionOnLastCapturedFrameAtLimit() { + final OpCodeTracerConfig config = + OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT) + .traceStorage(false) + .traceMemory(false) + .traceStack(false) + .limit(1) + .build(); + final DebugOperationTracer tracer = new DebugOperationTracer(config, false); + final MessageFrame frame = validMessageFrame(); + + traceFrame(frame, tracer, anOperation); + traceFrame(frame, tracer, anOperation); + + assertThat(tracer.getTraceFrames()).hasSize(1); + assertThat(tracer.getTraceFrames().get(0).getGasRemainingPostExecution()) + .isNotEqualTo(OptionalLong.empty()); + } + + @Test + void shouldCaptureAllFramesWhenLimitIsZero() { + final OpCodeTracerConfig config = + OpCodeTracerConfigBuilder.createFrom(OpCodeTracerConfig.DEFAULT) + .traceStorage(false) + .traceMemory(false) + .traceStack(false) + .limit(0) + .build(); + final DebugOperationTracer tracer = new DebugOperationTracer(config, false); + final MessageFrame frame = validMessageFrame(); + + for (int i = 0; i < 5; i++) { + traceFrame(frame, tracer, anOperation); + } + + assertThat(tracer.getTraceFrames()).hasSize(5); + } + private TraceFrame traceFrame(final MessageFrame frame) { return traceFrame( frame, diff --git a/evm/src/main/java/org/hyperledger/besu/evm/tracing/OpCodeTracerConfigBuilder.java b/evm/src/main/java/org/hyperledger/besu/evm/tracing/OpCodeTracerConfigBuilder.java index 9ba3f633231..3623239a32a 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/tracing/OpCodeTracerConfigBuilder.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/tracing/OpCodeTracerConfigBuilder.java @@ -27,6 +27,7 @@ public final class OpCodeTracerConfigBuilder { private boolean traceReturnData; private Set traceOpcodes; private boolean eip3155Strict; + private int limit; /** * Create an OpcodeTracerConfig builder from a previous built config. @@ -57,6 +58,7 @@ private OpCodeTracerConfigBuilder(final OpCodeTracerConfig opCodeTracerConfig) { this.traceReturnData = opCodeTracerConfig.traceReturnData(); this.traceOpcodes = opCodeTracerConfig.traceOpcodes(); this.eip3155Strict = opCodeTracerConfig.eip3155Strict(); + this.limit = opCodeTracerConfig.limit(); } /** @@ -114,6 +116,18 @@ public OpCodeTracerConfigBuilder traceOpcodes(final Set traceOpcodes) { return this; } + /** + * Set the maximum number of steps to trace. Zero means unlimited. + * + * @param n maximum number of steps, or 0 for unlimited + * @return the current builder + */ + public OpCodeTracerConfigBuilder limit(final int n) { + if (n < 0) throw new IllegalArgumentException("limit must be >= 0, got: " + n); + limit = n; + return this; + } + /** * Set eip3155Strict flag. * @@ -132,7 +146,7 @@ public OpCodeTracerConfigBuilder eip3155Strict(final boolean enable) { */ public OpCodeTracerConfig build() { return new Config( - traceStorage, traceMemory, traceStack, traceReturnData, traceOpcodes, eip3155Strict); + traceStorage, traceMemory, traceStack, traceReturnData, traceOpcodes, eip3155Strict, limit); } /** @@ -144,7 +158,7 @@ public OpCodeTracerConfig build() { public sealed interface OpCodeTracerConfig permits Config { /** static default OpcodeTracerConfig which can be accessed externally */ OpCodeTracerConfig DEFAULT = - new Config(true, false, true, false, Collections.emptySet(), false); + new Config(true, false, true, false, Collections.emptySet(), false, 0); /** * Check if tracing of storage is enabled. @@ -188,6 +202,13 @@ public sealed interface OpCodeTracerConfig permits Config { * @return true if enabled, false otherwise */ boolean eip3155Strict(); + + /** + * Maximum number of steps to trace. Zero means unlimited. + * + * @return the step limit, or 0 for unlimited + */ + int limit(); } private record Config( @@ -196,6 +217,7 @@ private record Config( boolean traceStack, boolean traceReturnData, Set traceOpcodes, - boolean eip3155Strict) + boolean eip3155Strict, + int limit) implements OpCodeTracerConfig {} }