Skip to content

Commit c2876f7

Browse files
Merge branch 'main' into feature/uint256-op-modulus
2 parents 7406569 + 0677923 commit c2876f7

File tree

7 files changed

+424
-26
lines changed

7 files changed

+424
-26
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand;
8181
import org.hyperledger.besu.cli.subcommands.storage.StorageSubCommand;
8282
import org.hyperledger.besu.cli.util.BesuCommandCustomFactory;
83+
import org.hyperledger.besu.cli.util.BootnodeResolver;
84+
import org.hyperledger.besu.cli.util.BootnodeResolver.BootnodeResolutionException;
8385
import org.hyperledger.besu.cli.util.CommandLineUtils;
8486
import org.hyperledger.besu.cli.util.ConfigDefaultValueProviderStrategy;
8587
import org.hyperledger.besu.cli.util.VersionProvider;
@@ -2237,7 +2239,13 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) {
22372239
List<EnodeURL> listBootNodes = null;
22382240
if (p2PDiscoveryOptions.bootNodes != null) {
22392241
try {
2240-
listBootNodes = buildEnodes(p2PDiscoveryOptions.bootNodes, getEnodeDnsConfiguration());
2242+
final List<String> resolvedBootNodeArgs =
2243+
BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes);
2244+
listBootNodes = buildEnodes(resolvedBootNodeArgs, getEnodeDnsConfiguration());
2245+
2246+
} catch (final BootnodeResolutionException e) {
2247+
throw new ParameterException(commandLine, e.getMessage(), e);
2248+
22412249
} catch (final IllegalArgumentException e) {
22422250
throw new ParameterException(commandLine, e.getMessage());
22432251
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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.cli.util;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
19+
import java.io.BufferedReader;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.net.URI;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.Paths;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
32+
/**
33+
* A utility class for resolving bootnodes from various sources such as URLs, files, or raw strings.
34+
*/
35+
public final class BootnodeResolver {
36+
37+
private static final Logger LOG = LoggerFactory.getLogger(BootnodeResolver.class);
38+
39+
private BootnodeResolver() {}
40+
41+
/**
42+
* Resolves a list of bootnodes from the provided sources.
43+
*
44+
* <p>The sources can be URLs (http, https, file), local file paths, or raw bootnode strings.
45+
* Comments (lines starting with '#') and empty lines are ignored.
46+
*
47+
* @param bootNodesList A list of bootnode sources (e.g., URLs, file paths, or raw strings).
48+
* @return A list of resolved bootnode strings.
49+
* @throws BootnodeResolutionException If an error occurs while resolving bootnodes.
50+
*/
51+
public static List<String> resolve(final List<String> bootNodesList) {
52+
final List<String> resolved = new ArrayList<>();
53+
if (bootNodesList == null || bootNodesList.isEmpty()) {
54+
return resolved;
55+
}
56+
57+
for (final String node : bootNodesList) {
58+
if (node == null || node.isBlank()) {
59+
continue;
60+
}
61+
62+
// Try parsing as URI to branch by scheme (http/https/file). Fallback to path/raw.
63+
URI uri = null;
64+
try {
65+
uri = URI.create(node);
66+
} catch (final IllegalArgumentException ignored) {
67+
// Not a URI; may be a local path or a raw enode. Handled below.
68+
}
69+
70+
final String scheme = (uri != null) ? uri.getScheme() : null;
71+
72+
if ("http".equalsIgnoreCase(scheme)
73+
|| "https".equalsIgnoreCase(scheme)
74+
|| "file".equalsIgnoreCase(scheme)) {
75+
// Remote list (URL)
76+
try (BufferedReader reader =
77+
new BufferedReader(new InputStreamReader(uri.toURL().openStream(), UTF_8))) {
78+
79+
final List<String> lines =
80+
reader
81+
.lines()
82+
.map(String::trim)
83+
.filter(l -> !l.isEmpty())
84+
.filter(l -> !l.startsWith("#"))
85+
.toList();
86+
87+
resolved.addAll(lines);
88+
89+
LOG.debug("Resolved bootnodes from URL: {}", node);
90+
91+
LOG.trace("Bootnodes fetched from {}: {}", node, lines);
92+
93+
} catch (final IOException e) {
94+
throw new BootnodeResolutionException(
95+
"Failed to fetch bootnodes from URL: " + node + "; " + e.getMessage(), e);
96+
}
97+
98+
} else {
99+
100+
final Path p = Paths.get(node);
101+
if (Files.exists(p)) {
102+
try {
103+
final List<String> lines = readPathLines(p);
104+
resolved.addAll(lines);
105+
LOG.debug("Resolved bootnodes from local file: {}", node);
106+
if (LOG.isTraceEnabled()) {
107+
LOG.trace("Bootnodes fetched from {}: {}", node, lines);
108+
}
109+
continue;
110+
} catch (final IOException e) {
111+
throw new BootnodeResolutionException(
112+
"Failed to read bootnodes from file: " + node + "; " + e.getMessage(), e);
113+
}
114+
}
115+
116+
resolved.add(node);
117+
LOG.debug("Using raw bootnode string: {}", node);
118+
}
119+
}
120+
121+
LOG.debug("Total resolved bootnodes: {}", resolved.size());
122+
return resolved;
123+
}
124+
125+
/**
126+
* Reads and processes lines from a file at the specified path.
127+
*
128+
* <p>Trims each line, removes empty lines, and ignores lines starting with '#'.
129+
*
130+
* @param path The path to the file.
131+
* @return A list of processed lines from the file.
132+
* @throws IOException If an error occurs while reading the file.
133+
*/
134+
private static List<String> readPathLines(final Path path) throws IOException {
135+
try (var lines = Files.lines(path, UTF_8)) {
136+
return lines
137+
.map(String::trim)
138+
.filter(l -> !l.isEmpty())
139+
.filter(l -> !l.startsWith("#"))
140+
.toList();
141+
}
142+
}
143+
144+
/** Exception thrown when there is an error resolving bootnodes. */
145+
public static final class BootnodeResolutionException extends RuntimeException {
146+
/**
147+
* Constructs a new BootnodeResolutionException with the specified detail message and cause.
148+
*
149+
* @param message The detail message.
150+
* @param cause The cause of the exception.
151+
*/
152+
public BootnodeResolutionException(final String message, final Throwable cause) {
153+
super(message, cause);
154+
}
155+
}
156+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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.cli.util;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.io.BufferedReader;
22+
import java.io.IOException;
23+
import java.io.InputStreamReader;
24+
import java.net.InetSocketAddress;
25+
import java.net.URI;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.util.List;
29+
30+
import com.sun.net.httpserver.HttpExchange;
31+
import com.sun.net.httpserver.HttpHandler;
32+
import com.sun.net.httpserver.HttpServer;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.io.TempDir;
36+
37+
public class BootnodeResolverTest {
38+
39+
private static final String VALID_NODE_ID =
40+
"0x000000000000000000000000000000000000000000000000000000000000000a";
41+
42+
private HttpServer server;
43+
44+
@AfterEach
45+
void stopServer() {
46+
if (server != null) {
47+
server.stop(0);
48+
}
49+
}
50+
51+
@Test
52+
void shouldAcceptBootnodesFromLocalFile(@TempDir final Path tempDir) throws Exception {
53+
final Path file = tempDir.resolve("enodes.txt");
54+
Files.write(
55+
file,
56+
List.of(
57+
"enode://" + VALID_NODE_ID + "@192.168.0.1:4567",
58+
"",
59+
"enode://" + VALID_NODE_ID + "@192.168.0.2:4567"),
60+
UTF_8);
61+
62+
final List<String> result = BootnodeResolver.resolve(List.of(file.toString()));
63+
64+
assertThat(result)
65+
.containsExactly(
66+
"enode://" + VALID_NODE_ID + "@192.168.0.1:4567",
67+
"enode://" + VALID_NODE_ID + "@192.168.0.2:4567");
68+
}
69+
70+
@Test
71+
void shouldAcceptBootnodesFromFileUri(@TempDir final Path tempDir) throws Exception {
72+
final Path file = tempDir.resolve("enodes.txt");
73+
Files.writeString(file, "enode://" + VALID_NODE_ID + "@10.0.0.1:30303\n", UTF_8);
74+
75+
final List<String> result = BootnodeResolver.resolve(List.of(file.toUri().toString()));
76+
77+
assertThat(result).containsExactly("enode://" + VALID_NODE_ID + "@10.0.0.1:30303");
78+
}
79+
80+
@Test
81+
void shouldAcceptBootnodesFromHttpUrl() throws Exception {
82+
server = HttpServer.create(new InetSocketAddress(0), 0);
83+
server.createContext(
84+
"/enodes.txt",
85+
new HttpHandler() {
86+
@Override
87+
public void handle(HttpExchange exchange) throws IOException {
88+
final byte[] body =
89+
("enode://"
90+
+ VALID_NODE_ID
91+
+ "@11.11.11.11:30303\n"
92+
+ "enode://"
93+
+ VALID_NODE_ID
94+
+ "@22.22.22.22:30303\n")
95+
.getBytes(UTF_8);
96+
exchange.sendResponseHeaders(200, body.length);
97+
exchange.getResponseBody().write(body);
98+
exchange.close();
99+
}
100+
});
101+
server.start();
102+
103+
final String url = "http://localhost:" + server.getAddress().getPort() + "/enodes.txt";
104+
105+
final List<String> result = BootnodeResolver.resolve(List.of(url));
106+
107+
assertThat(result)
108+
.containsExactly(
109+
"enode://" + VALID_NODE_ID + "@11.11.11.11:30303",
110+
"enode://" + VALID_NODE_ID + "@22.22.22.22:30303");
111+
}
112+
113+
@Test
114+
void shouldAcceptBootnodesAsRawStrings() {
115+
final String raw = "enode://" + VALID_NODE_ID + "@33.33.33.33:30303";
116+
117+
final List<String> result = BootnodeResolver.resolve(List.of(raw));
118+
119+
assertThat(result).containsExactly(raw);
120+
}
121+
122+
@Test
123+
void fileUri_openStream_readsLines(@TempDir final Path tempDir) throws Exception {
124+
125+
final Path file = tempDir.resolve("enodes.txt");
126+
Files.writeString(
127+
file,
128+
"enode://"
129+
+ VALID_NODE_ID
130+
+ "@10.0.0.1:30303\n"
131+
+ "# comment\n"
132+
+ "\n"
133+
+ "enode://"
134+
+ VALID_NODE_ID
135+
+ "@10.0.0.2:30303\n",
136+
UTF_8);
137+
138+
final URI uri = file.toUri();
139+
final List<String> lines;
140+
try (var in = uri.toURL().openStream();
141+
var br = new BufferedReader(new InputStreamReader(in, UTF_8))) {
142+
lines =
143+
br.lines()
144+
.map(String::trim)
145+
.filter(l -> !l.isEmpty())
146+
.filter(l -> !l.startsWith("#"))
147+
.toList();
148+
}
149+
150+
assertThat(lines)
151+
.containsExactly(
152+
"enode://" + VALID_NODE_ID + "@10.0.0.1:30303",
153+
"enode://" + VALID_NODE_ID + "@10.0.0.2:30303");
154+
}
155+
156+
@Test
157+
void shouldThrowWhenBootnodesUrlIsUnreachable() {
158+
assertThatThrownBy(
159+
() -> BootnodeResolver.resolve(List.of("http://localhost:65535/missing.txt")))
160+
.isInstanceOf(BootnodeResolver.BootnodeResolutionException.class);
161+
}
162+
}

ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
4848
import org.hyperledger.besu.ethereum.mainnet.ScheduleBasedBlockHeaderFunctions;
4949
import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor;
50+
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessListFactory;
5051
import org.hyperledger.besu.ethereum.mainnet.feemarket.ExcessBlobGasCalculator;
5152
import org.hyperledger.besu.ethereum.mainnet.requests.RequestProcessingContext;
5253
import org.hyperledger.besu.ethereum.mainnet.requests.RequestProcessorCoordinator;
@@ -194,7 +195,10 @@ protected BlockCreationResult createBlock(
194195
final ProtocolSpec newProtocolSpec =
195196
protocolSchedule.getForNextBlockHeader(parentHeader, timestamp);
196197
final boolean includeBlockAccessList =
197-
newProtocolSpec.getBlockAccessListFactory().isPresent();
198+
newProtocolSpec
199+
.getBlockAccessListFactory()
200+
.filter(BlockAccessListFactory::isForkActivated)
201+
.isPresent();
198202

199203
final ProcessableBlockHeader processableBlockHeader =
200204
createPending(

0 commit comments

Comments
 (0)