Skip to content

Commit b2cbdea

Browse files
Stream debug_traceBlock* responses directly to avoid OOM on large blocks (#9848)
* stream block traces on op code level Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * correctly parse default setting for memory tracing Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * fix initcode capture for failed create op codes Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * created separate streaming debug tracer, for batch request fall back to accumulation in memory, adddress pr comments Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * execute tests from genesis and verify full trace Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * addressed pr comments Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * spotless Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * optimize trace streaming and struct log handling Signed-off-by: Ameziane H. <ameziane.hamlat@consensys.net> * spotless Signed-off-by: Ameziane H. <ameziane.hamlat@consensys.net> * Fix remaining issues and add unit tests Signed-off-by: Ameziane H. <ameziane.hamlat@consensys.net> * added back pressure when writing to the socket and reduced the buffer size to work better with netty's default buffer size Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * improve error handling by deferring to send the header only when data is available, allows to send the proper error codes during setup Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * compactHex candidate comparison Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * wire in more performant hex writer Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * introduce separate timeout for streaming calls, defaults to 10 minutes Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * spotless Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * Fix streamin/accumulating output parity, added missing refund field, corrected error format, reason encoding, returnValue prefix, and precompile gasCost, with equivalence tests between both Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * revert accidental removal of 0x prefix Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> * pad memory bytes to 32 bytes Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> --------- Signed-off-by: daniellehrner <daniel.lehrner@consensys.net> Signed-off-by: Ameziane H. <ameziane.hamlat@consensys.net> Co-authored-by: Ameziane H. <ameziane.hamlat@consensys.net>
1 parent 5a7d949 commit b2cbdea

File tree

49 files changed

+3623
-656
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3623
-656
lines changed

app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,7 +1917,10 @@ private void configure() throws Exception {
19171917

19181918
jsonRpcConfiguration =
19191919
jsonRpcHttpOptions.jsonRpcConfiguration(
1920-
hostsAllowlist, p2PDiscoveryConfig.p2pHost(), unstableRPCOptions.getHttpTimeoutSec());
1920+
hostsAllowlist,
1921+
p2PDiscoveryConfig.p2pHost(),
1922+
unstableRPCOptions.getHttpTimeoutSec(),
1923+
unstableRPCOptions.getHttpStreamingTimeoutSec());
19211924
logger.info("RPC HTTP JSON-RPC config: {}", jsonRpcConfiguration);
19221925
if (isEngineApiEnabled()) {
19231926
engineJsonRpcConfiguration = createEngineJsonRpcConfiguration();
@@ -2086,7 +2089,8 @@ private JsonRpcConfiguration createEngineJsonRpcConfiguration() {
20862089
jsonRpcHttpOptions.jsonRpcConfiguration(
20872090
engineRPCConfig.engineHostsAllowlist(),
20882091
p2PDiscoveryConfig.p2pHost(),
2089-
unstableRPCOptions.getHttpTimeoutSec());
2092+
unstableRPCOptions.getHttpTimeoutSec(),
2093+
unstableRPCOptions.getHttpStreamingTimeoutSec());
20902094
engineConfig.setPort(engineRPCConfig.engineRpcPort());
20912095
engineConfig.setRpcApis(Arrays.asList("ENGINE", "ETH"));
20922096
engineConfig.setEnabled(isEngineApiEnabled());

app/src/main/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptions.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,17 +302,22 @@ public JsonRpcConfiguration jsonRpcConfiguration() {
302302
* @param hostsAllowlist List of hosts allowed
303303
* @param defaultHostAddress Default host address
304304
* @param timeoutSec timeout in seconds
305+
* @param streamingTimeoutSec timeout in seconds for streaming methods
305306
* @return A JsonRpcConfiguration instance
306307
*/
307308
public JsonRpcConfiguration jsonRpcConfiguration(
308-
final List<String> hostsAllowlist, final String defaultHostAddress, final Long timeoutSec) {
309+
final List<String> hostsAllowlist,
310+
final String defaultHostAddress,
311+
final Long timeoutSec,
312+
final Long streamingTimeoutSec) {
309313

310314
final JsonRpcConfiguration jsonRpcConfiguration = this.jsonRpcConfiguration();
311315

312316
jsonRpcConfiguration.setHost(
313317
Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost);
314318
jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist);
315319
jsonRpcConfiguration.setHttpTimeoutSec(timeoutSec);
320+
jsonRpcConfiguration.setHttpStreamingTimeoutSec(streamingTimeoutSec);
316321
return jsonRpcConfiguration;
317322
}
318323

app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package org.hyperledger.besu.cli.options;
1616

1717
import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
18+
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;
1819

1920
import picocli.CommandLine;
2021

@@ -27,6 +28,14 @@ public class RPCOptions {
2728
description = "HTTP timeout in seconds (default: ${DEFAULT-VALUE})")
2829
private final Long httpTimeoutSec = TimeoutOptions.defaultOptions().getTimeoutSeconds();
2930

31+
@CommandLine.Option(
32+
hidden = true,
33+
names = {"--Xhttp-streaming-timeout-seconds"},
34+
description =
35+
"HTTP timeout in seconds for streaming methods like debug_traceBlock (default: ${DEFAULT-VALUE})")
36+
private final Long httpStreamingTimeoutSec =
37+
JsonRpcConfiguration.DEFAULT_HTTP_STREAMING_TIMEOUT_SEC;
38+
3039
@CommandLine.Option(
3140
hidden = true,
3241
names = {"--Xws-timeout-seconds"},
@@ -54,6 +63,15 @@ public Long getHttpTimeoutSec() {
5463
return httpTimeoutSec;
5564
}
5665

66+
/**
67+
* Gets http streaming timeout sec.
68+
*
69+
* @return the http streaming timeout sec
70+
*/
71+
public Long getHttpStreamingTimeoutSec() {
72+
return httpStreamingTimeoutSec;
73+
}
74+
5775
/**
5876
* Gets WebSocket timeout sec.
5977
*

ethereum/api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ dependencies {
8383
implementation 'com.github.ben-manes.caffeine:caffeine'
8484

8585
annotationProcessor "org.immutables:value"
86+
annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
8687
implementation "org.immutables:value-annotations"
8788

8889
runtimeOnly 'org.bouncycastle:bcpkix-jdk18on'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.methods;
16+
17+
import java.util.Random;
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.openjdk.jmh.annotations.Benchmark;
21+
import org.openjdk.jmh.annotations.BenchmarkMode;
22+
import org.openjdk.jmh.annotations.Fork;
23+
import org.openjdk.jmh.annotations.Measurement;
24+
import org.openjdk.jmh.annotations.Mode;
25+
import org.openjdk.jmh.annotations.OperationsPerInvocation;
26+
import org.openjdk.jmh.annotations.OutputTimeUnit;
27+
import org.openjdk.jmh.annotations.Param;
28+
import org.openjdk.jmh.annotations.Scope;
29+
import org.openjdk.jmh.annotations.Setup;
30+
import org.openjdk.jmh.annotations.State;
31+
import org.openjdk.jmh.annotations.Warmup;
32+
import org.openjdk.jmh.infra.Blackhole;
33+
34+
/**
35+
* Benchmarks {@link HexWriter#encodeTo}. Each invocation encodes {@link #N} values to simulate a
36+
* realistic per-opcode workload (stack entries).
37+
*
38+
* <p>Run with: {@code ./gradlew :ethereum:api:jmh -Pincludes=CompactHexBenchmark}
39+
*/
40+
@State(Scope.Thread)
41+
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
42+
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
43+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
44+
@BenchmarkMode(Mode.AverageTime)
45+
@Fork(3)
46+
public class CompactHexBenchmark {
47+
48+
private static final int N = 16;
49+
private static final int BUF_SIZE = 32 * 1024;
50+
51+
public enum ValueSize {
52+
BYTES_1(1),
53+
BYTES_8(8),
54+
BYTES_20(20),
55+
BYTES_32(32);
56+
57+
final int size;
58+
59+
ValueSize(final int size) {
60+
this.size = size;
61+
}
62+
}
63+
64+
@Param({"BYTES_1", "BYTES_8", "BYTES_20", "BYTES_32"})
65+
private ValueSize valueSize;
66+
67+
private byte[][] values;
68+
private byte[] writeBuf;
69+
70+
@Setup
71+
public void setup() {
72+
final Random rng = new Random(42);
73+
values = new byte[N][];
74+
for (int i = 0; i < N; i++) {
75+
values[i] = new byte[valueSize.size];
76+
rng.nextBytes(values[i]);
77+
// ~25% of values have leading zero bytes (realistic for stack values < 256 bits)
78+
if (i % 4 == 0 && values[i].length > 1) {
79+
values[i][0] = 0;
80+
}
81+
}
82+
writeBuf = new byte[BUF_SIZE];
83+
}
84+
85+
@Benchmark
86+
@OperationsPerInvocation(N)
87+
public void encodeTo(final Blackhole bh) {
88+
int pos = 0;
89+
for (final byte[] bytes : values) {
90+
pos = HexWriter.encodeTo(bytes, bytes.length, writeBuf, pos, true);
91+
}
92+
bh.consume(pos);
93+
}
94+
}

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/AbstractJsonRpcExecutor.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
public abstract class AbstractJsonRpcExecutor {
4444
private static final Logger LOG = LoggerFactory.getLogger(AbstractJsonRpcExecutor.class);
4545

46-
private static final String SPAN_CONTEXT = "span_context";
46+
protected static final String SPAN_CONTEXT = "span_context";
4747
final JsonRpcExecutor jsonRpcExecutor;
4848
final Tracer tracer;
4949
final RoutingContext ctx;
@@ -97,16 +97,24 @@ protected static void handleJsonRpcError(
9797
final RoutingContext routingContext, final Object id, final RpcErrorType error) {
9898
final HttpServerResponse response = routingContext.response();
9999
if (!response.closed()) {
100-
response
101-
.setStatusCode(statusCodeFromError(error).code())
102-
.end(Json.encode(new JsonRpcErrorResponse(id, error)));
100+
if (response.headWritten()) {
101+
// Streaming already started — cannot change status code or headers.
102+
// Reset the connection so the client sees a transport error rather than
103+
// silently receiving truncated JSON.
104+
response.reset();
105+
} else {
106+
response
107+
.setStatusCode(statusCodeFromError(error).code())
108+
.end(Json.encode(new JsonRpcErrorResponse(id, error)));
109+
}
103110
}
104111
}
105112

106113
private static HttpResponseStatus statusCodeFromError(final RpcErrorType error) {
107114
return switch (error) {
108115
case INVALID_REQUEST, PARSE_ERROR -> HttpResponseStatus.BAD_REQUEST;
109116
case TIMEOUT_ERROR -> HttpResponseStatus.REQUEST_TIMEOUT;
117+
case UNAUTHORIZED -> HttpResponseStatus.UNAUTHORIZED;
110118
default -> HttpResponseStatus.OK;
111119
};
112120
}

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/HandlerFactory.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.util.Collection;
2323
import java.util.Map;
2424
import java.util.Optional;
25-
import java.util.function.Function;
2625
import java.util.stream.Collectors;
2726

2827
import io.opentelemetry.api.trace.Tracer;
@@ -34,10 +33,15 @@ public class HandlerFactory {
3433
public static Handler<RoutingContext> timeout(
3534
final TimeoutOptions globalOptions, final Map<String, JsonRpcMethod> methods) {
3635
assert methods != null && globalOptions != null;
36+
// Only explicitly registered non-streaming methods get a
37+
// timeout from this handler. Streaming methods are excluded because they can
38+
// run much longer than the default 30s. Their timeout is managed by JsonRpcExecutorHandler
39+
// instead.
3740
return TimeoutHandler.handler(
38-
Optional.of(globalOptions),
39-
methods.keySet().stream()
40-
.collect(Collectors.toMap(Function.identity(), ignored -> globalOptions)));
41+
Optional.empty(),
42+
methods.entrySet().stream()
43+
.filter(e -> !e.getValue().isStreaming())
44+
.collect(Collectors.toMap(Map.Entry::getKey, ignored -> globalOptions)));
4145
}
4246

4347
public static Handler<RoutingContext> authentication(

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/handlers/JsonRpcExecutorHandler.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import io.opentelemetry.api.trace.Tracer;
2828
import io.vertx.core.Handler;
29+
import io.vertx.core.json.JsonObject;
2930
import io.vertx.ext.web.RoutingContext;
3031
import org.slf4j.Logger;
3132
import org.slf4j.LoggerFactory;
@@ -40,7 +41,7 @@ public static Handler<RoutingContext> handler(
4041
final Tracer tracer,
4142
final JsonRpcConfiguration jsonRpcConfiguration) {
4243
return ctx -> {
43-
long timeoutMillis = jsonRpcConfiguration.getHttpTimeoutSec() * 1000;
44+
final long timeoutMillis = resolveTimeoutMillis(ctx, jsonRpcExecutor, jsonRpcConfiguration);
4445
final long timerId =
4546
ctx.vertx()
4647
.setTimer(
@@ -147,4 +148,17 @@ private static boolean isJsonObjectRequest(final RoutingContext ctx) {
147148
private static boolean isJsonArrayRequest(final RoutingContext ctx) {
148149
return ctx.data().containsKey(ContextKey.REQUEST_BODY_AS_JSON_ARRAY.name());
149150
}
151+
152+
private static long resolveTimeoutMillis(
153+
final RoutingContext ctx,
154+
final JsonRpcExecutor jsonRpcExecutor,
155+
final JsonRpcConfiguration config) {
156+
if (isJsonObjectRequest(ctx)) {
157+
final JsonObject req = ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name());
158+
if (req != null && jsonRpcExecutor.isStreamingMethod(req.getString("method"))) {
159+
return config.getHttpStreamingTimeoutSec() * 1000;
160+
}
161+
}
162+
return config.getHttpTimeoutSec() * 1000;
163+
}
150164
}

0 commit comments

Comments
 (0)