Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb99511
stream block traces on op code level
daniellehrner Feb 19, 2026
44dfe5d
correctly parse default setting for memory tracing
daniellehrner Feb 19, 2026
8ce82d9
fix initcode capture for failed create op codes
daniellehrner Feb 20, 2026
67326b4
created separate streaming debug tracer, for batch request fall back …
daniellehrner Mar 5, 2026
97ddb72
execute tests from genesis and verify full trace
daniellehrner Mar 11, 2026
42c1d66
Merge branch 'main' into feat/trace-streaming
daniellehrner Mar 17, 2026
e287713
addressed pr comments
daniellehrner Mar 17, 2026
1ecdc13
spotless
daniellehrner Mar 17, 2026
a834373
Merge branch 'main' into feat/trace-streaming
daniellehrner Mar 17, 2026
ed40fe1
optimize trace streaming and struct log handling
ahamlat Mar 24, 2026
5649008
spotless
ahamlat Mar 24, 2026
073d1c1
Merge branch 'main' into feat/trace-streaming
ahamlat Mar 24, 2026
8de61b1
Fix remaining issues and add unit tests
ahamlat Mar 25, 2026
32b4af9
Merge remote-tracking branch 'origin/main' into feat/trace-streaming
daniellehrner Mar 30, 2026
01c5f4d
added back pressure when writing to the socket and reduced the buffer…
daniellehrner Mar 30, 2026
8f2af65
improve error handling by deferring to send the header only when data…
daniellehrner Mar 31, 2026
fe60790
Merge branch 'main' into feat/trace-streaming
daniellehrner Mar 31, 2026
c8d0070
compactHex candidate comparison
daniellehrner Mar 31, 2026
387c536
wire in more performant hex writer
daniellehrner Mar 31, 2026
72c9a79
introduce separate timeout for streaming calls, defaults to 10 minutes
daniellehrner Mar 31, 2026
80c22f4
Merge branch 'main' into feat/trace-streaming
daniellehrner Mar 31, 2026
c22e158
spotless
daniellehrner Mar 31, 2026
8655ffb
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 2, 2026
1aa41c0
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 2, 2026
dfde7fe
Fix streamin/accumulating output parity, added missing refund field, …
daniellehrner Apr 3, 2026
93ddf28
revert accidental removal of 0x prefix
daniellehrner Apr 3, 2026
06fe72c
pad memory bytes to 32 bytes
daniellehrner Apr 4, 2026
58f9924
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 4, 2026
5a059ce
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 7, 2026
027af92
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 8, 2026
266d5c7
Merge branch 'main' into feat/trace-streaming
daniellehrner Apr 8, 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
8 changes: 6 additions & 2 deletions app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,10 @@ private void configure() throws Exception {

jsonRpcConfiguration =
jsonRpcHttpOptions.jsonRpcConfiguration(
hostsAllowlist, p2PDiscoveryConfig.p2pHost(), unstableRPCOptions.getHttpTimeoutSec());
hostsAllowlist,
p2PDiscoveryConfig.p2pHost(),
unstableRPCOptions.getHttpTimeoutSec(),
unstableRPCOptions.getHttpStreamingTimeoutSec());
logger.info("RPC HTTP JSON-RPC config: {}", jsonRpcConfiguration);
if (isEngineApiEnabled()) {
engineJsonRpcConfiguration = createEngineJsonRpcConfiguration();
Expand Down Expand Up @@ -2086,7 +2089,8 @@ private JsonRpcConfiguration createEngineJsonRpcConfiguration() {
jsonRpcHttpOptions.jsonRpcConfiguration(
engineRPCConfig.engineHostsAllowlist(),
p2PDiscoveryConfig.p2pHost(),
unstableRPCOptions.getHttpTimeoutSec());
unstableRPCOptions.getHttpTimeoutSec(),
unstableRPCOptions.getHttpStreamingTimeoutSec());
engineConfig.setPort(engineRPCConfig.engineRpcPort());
engineConfig.setRpcApis(Arrays.asList("ENGINE", "ETH"));
engineConfig.setEnabled(isEngineApiEnabled());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,17 +302,22 @@ public JsonRpcConfiguration jsonRpcConfiguration() {
* @param hostsAllowlist List of hosts allowed
* @param defaultHostAddress Default host address
* @param timeoutSec timeout in seconds
* @param streamingTimeoutSec timeout in seconds for streaming methods
* @return A JsonRpcConfiguration instance
*/
public JsonRpcConfiguration jsonRpcConfiguration(
final List<String> hostsAllowlist, final String defaultHostAddress, final Long timeoutSec) {
final List<String> hostsAllowlist,
final String defaultHostAddress,
final Long timeoutSec,
final Long streamingTimeoutSec) {

final JsonRpcConfiguration jsonRpcConfiguration = this.jsonRpcConfiguration();

jsonRpcConfiguration.setHost(
Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost);
jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist);
jsonRpcConfiguration.setHttpTimeoutSec(timeoutSec);
jsonRpcConfiguration.setHttpStreamingTimeoutSec(streamingTimeoutSec);
return jsonRpcConfiguration;
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/org/hyperledger/besu/cli/options/RPCOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.hyperledger.besu.cli.options;

import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration;

import picocli.CommandLine;

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

@CommandLine.Option(
hidden = true,
names = {"--Xhttp-streaming-timeout-seconds"},
description =
"HTTP timeout in seconds for streaming methods like debug_traceBlock (default: ${DEFAULT-VALUE})")
private final Long httpStreamingTimeoutSec =
JsonRpcConfiguration.DEFAULT_HTTP_STREAMING_TIMEOUT_SEC;

@CommandLine.Option(
hidden = true,
names = {"--Xws-timeout-seconds"},
Expand Down Expand Up @@ -54,6 +63,15 @@ public Long getHttpTimeoutSec() {
return httpTimeoutSec;
}

/**
* Gets http streaming timeout sec.
*
* @return the http streaming timeout sec
*/
public Long getHttpStreamingTimeoutSec() {
return httpStreamingTimeoutSec;
}

/**
* Gets WebSocket timeout sec.
*
Expand Down
1 change: 1 addition & 0 deletions ethereum/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ dependencies {
implementation 'com.github.ben-manes.caffeine:caffeine'

annotationProcessor "org.immutables:value"
annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
implementation "org.immutables:value-annotations"

runtimeOnly 'org.bouncycastle:bcpkix-jdk18on'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.methods;

import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;

/**
* Benchmarks {@link HexWriter#encodeTo}. Each invocation encodes {@link #N} values to simulate a
* realistic per-opcode workload (stack entries).
*
* <p>Run with: {@code ./gradlew :ethereum:api:jmh -Pincludes=CompactHexBenchmark}
*/
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Fork(3)
public class CompactHexBenchmark {

private static final int N = 16;
private static final int BUF_SIZE = 32 * 1024;

public enum ValueSize {
BYTES_1(1),
BYTES_8(8),
BYTES_20(20),
BYTES_32(32);

final int size;

ValueSize(final int size) {
this.size = size;
}
}

@Param({"BYTES_1", "BYTES_8", "BYTES_20", "BYTES_32"})
private ValueSize valueSize;

private byte[][] values;
private byte[] writeBuf;

@Setup
public void setup() {
final Random rng = new Random(42);
values = new byte[N][];
for (int i = 0; i < N; i++) {
values[i] = new byte[valueSize.size];
rng.nextBytes(values[i]);
// ~25% of values have leading zero bytes (realistic for stack values < 256 bits)
if (i % 4 == 0 && values[i].length > 1) {
values[i][0] = 0;
}
}
writeBuf = new byte[BUF_SIZE];
}

@Benchmark
@OperationsPerInvocation(N)
public void encodeTo(final Blackhole bh) {
int pos = 0;
for (final byte[] bytes : values) {
pos = HexWriter.encodeTo(bytes, bytes.length, writeBuf, pos, true);
}
bh.consume(pos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
public abstract class AbstractJsonRpcExecutor {
private static final Logger LOG = LoggerFactory.getLogger(AbstractJsonRpcExecutor.class);

private static final String SPAN_CONTEXT = "span_context";
protected static final String SPAN_CONTEXT = "span_context";
final JsonRpcExecutor jsonRpcExecutor;
final Tracer tracer;
final RoutingContext ctx;
Expand Down Expand Up @@ -97,16 +97,24 @@ protected static void handleJsonRpcError(
final RoutingContext routingContext, final Object id, final RpcErrorType error) {
final HttpServerResponse response = routingContext.response();
if (!response.closed()) {
response
.setStatusCode(statusCodeFromError(error).code())
.end(Json.encode(new JsonRpcErrorResponse(id, error)));
if (response.headWritten()) {
// Streaming already started — cannot change status code or headers.
// Reset the connection so the client sees a transport error rather than
// silently receiving truncated JSON.
response.reset();
} else {
response
.setStatusCode(statusCodeFromError(error).code())
.end(Json.encode(new JsonRpcErrorResponse(id, error)));
}
}
}

private static HttpResponseStatus statusCodeFromError(final RpcErrorType error) {
return switch (error) {
case INVALID_REQUEST, PARSE_ERROR -> HttpResponseStatus.BAD_REQUEST;
case TIMEOUT_ERROR -> HttpResponseStatus.REQUEST_TIMEOUT;
case UNAUTHORIZED -> HttpResponseStatus.UNAUTHORIZED;
default -> HttpResponseStatus.OK;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

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

public static Handler<RoutingContext> authentication(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import io.opentelemetry.api.trace.Tracer;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -40,7 +41,7 @@ public static Handler<RoutingContext> handler(
final Tracer tracer,
final JsonRpcConfiguration jsonRpcConfiguration) {
return ctx -> {
long timeoutMillis = jsonRpcConfiguration.getHttpTimeoutSec() * 1000;
final long timeoutMillis = resolveTimeoutMillis(ctx, jsonRpcExecutor, jsonRpcConfiguration);
final long timerId =
ctx.vertx()
.setTimer(
Expand Down Expand Up @@ -147,4 +148,17 @@ private static boolean isJsonObjectRequest(final RoutingContext ctx) {
private static boolean isJsonArrayRequest(final RoutingContext ctx) {
return ctx.data().containsKey(ContextKey.REQUEST_BODY_AS_JSON_ARRAY.name());
}

private static long resolveTimeoutMillis(
final RoutingContext ctx,
final JsonRpcExecutor jsonRpcExecutor,
final JsonRpcConfiguration config) {
if (isJsonObjectRequest(ctx)) {
final JsonObject req = ctx.get(ContextKey.REQUEST_BODY_AS_JSON_OBJECT.name());
if (req != null && jsonRpcExecutor.isStreamingMethod(req.getString("method"))) {
return config.getHttpStreamingTimeoutSec() * 1000;
}
}
return config.getHttpTimeoutSec() * 1000;
}
}
Loading
Loading