Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ are provided with different values, using input as per the execution-apis spec i
- Add ability to pass a custom tracer to block simulation [#9708](https://github.com/hyperledger/besu/pull/9708)
- Add support for `4byteTracer` in `debug_trace*` methods to collect function selectors from internal calls via PR [#9642](https://github.com/hyperledger/besu/pull/9642). Thanks to [@JukLee0ira](https://github.com/JukLee0ira).
- Update assertj to v3.27.7 [#9710](https://github.com/hyperledger/besu/pull/9710)
- Update rocksdbjni lib version from 9.7.3 to 10.6.2 [#9767](https://github.com/hyperledger/besu/pull/9767)
- Update vertx to 4.5.24 [#9645](https://github.com/hyperledger/besu/pull/9645)
- Add byte-level metrics for P2P message exchange [#9666](https://github.com/hyperledger/besu/pull/9666)
- Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873)
Expand Down
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