Skip to content

Commit 53781b2

Browse files
authored
Add ENR v5 bootnodes support for DiscV5 discovery (#9970)
Add ENR (enr:) bootnode support for DiscV5 discovery by introducing a new v5Bootnodes section in genesis config alongside the existing bootnodes (enode) section. Genesis config changes: - Add v5Bootnodes to config.discovery for ENR-format bootnodes - Add getV5BootNodes() to DiscoveryOptions to parse the new section - Populate mainnet.json with 17 ENR bootnodes and hoodi.json with 9 Bootnode parsing simplification: - Use --Xv5-discovery-enabled flag to determine expected format (ENR vs enode) instead of prefix detection heuristic - EthNetworkConfig reads bootnodes and v5Bootnodes independently - CLI --bootnodes overrides the active protocol's list and clears the unused protocol's list; genesis defaults keep both intact - Replace heuristic fallback in getBootnodeIdentifiers() with explicit discoveryV5Enabled flag check DiscV5 peer connection pipeline fixes: - Handle compressed public keys (33-byte SEC1) in NodeKeySigner.deriveECDHKeyAgreement - Fix candidatePeers() to use isListening() instead of isReadyForConnections() which requires DiscV4 bonding status - Filter out the local node record from streamLiveNodes() - Explicitly use secp256k1 for ENR identity scheme v4 Cleanup: - Remove dead MAINNET fallback null-checks in RunnerBuilder --------- Signed-off-by: Usman Saleem <usman@usmans.info>
1 parent fd9b355 commit 53781b2

File tree

14 files changed

+332
-86
lines changed

14 files changed

+332
-86
lines changed

app/src/main/java/org/hyperledger/besu/RunnerBuilder.java

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import org.hyperledger.besu.cli.config.EthNetworkConfig;
2424
import org.hyperledger.besu.cli.options.EthstatsOptions;
25-
import org.hyperledger.besu.config.NetworkDefinition;
2625
import org.hyperledger.besu.controller.BesuController;
2726
import org.hyperledger.besu.cryptoservices.NodeKey;
2827
import org.hyperledger.besu.ethereum.ProtocolContext;
@@ -672,25 +671,19 @@ public Runner build() {
672671
});
673672
discoveryConfiguration.setPreferIpv6Outbound(preferIpv6Outbound);
674673
if (discoveryEnabled) {
675-
final List<EnodeURLImpl> bootstrap;
676-
if (ethNetworkConfig.enodeBootNodes() == null) {
677-
bootstrap = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enodeBootNodes();
678-
} else {
679-
bootstrap = ethNetworkConfig.enodeBootNodes();
680-
}
681-
discoveryConfiguration.setEnodeBootnodes(bootstrap);
682-
discoveryConfiguration.setEnrBootnodes(
683-
ethNetworkConfig.enrBootNodes() == null
684-
? EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enrBootNodes()
685-
: ethNetworkConfig.enrBootNodes());
674+
discoveryConfiguration.setEnodeBootnodes(ethNetworkConfig.enodeBootNodes());
675+
discoveryConfiguration.setEnrBootnodes(ethNetworkConfig.enrBootNodes());
686676

687677
discoveryConfiguration.setIncludeBootnodesOnPeerRefresh(
688678
besuController.getGenesisConfigOptions().isPoa() && poaDiscoveryRetryBootnodes);
689679
LOG.info(
690680
"Resolved {} bootnodes.",
691681
discoveryConfiguration.getEnodeBootnodes().size()
692682
+ discoveryConfiguration.getEnrBootnodes().size());
693-
LOG.debug("Bootnodes = {}", bootstrap);
683+
LOG.debug(
684+
"Bootnodes enode={}, enr={}",
685+
discoveryConfiguration.getEnodeBootnodes(),
686+
discoveryConfiguration.getEnrBootnodes());
694687
discoveryConfiguration.setDnsDiscoveryURL(ethNetworkConfig.dnsDiscoveryUrl());
695688
discoveryConfiguration.setDiscoveryV5Enabled(
696689
networkingConfiguration.discoveryConfiguration().isDiscoveryV5Enabled());

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

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
import org.hyperledger.besu.cli.util.VersionProvider;
8989
import org.hyperledger.besu.components.BesuComponent;
9090
import org.hyperledger.besu.config.CheckpointConfigOptions;
91+
import org.hyperledger.besu.config.DiscoveryOptions;
9192
import org.hyperledger.besu.config.GenesisConfig;
9293
import org.hyperledger.besu.config.GenesisConfigOptions;
9394
import org.hyperledger.besu.config.JsonUtil;
@@ -2517,6 +2518,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) {
25172518

25182519
if (p2PDiscoveryOptions.bootNodes == null) {
25192520
builder.setEnodeBootNodes(new ArrayList<>());
2521+
builder.setEnrBootNodes(new ArrayList<>());
25202522
}
25212523
builder.setDnsDiscoveryUrl(null);
25222524
}
@@ -2539,46 +2541,75 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) {
25392541
discoveryDnsUrlFromGenesis.ifPresent(builder::setDnsDiscoveryUrl);
25402542
}
25412543

2542-
List<EnodeURLImpl> listBootNodes = null;
2543-
if (p2PDiscoveryOptions.bootNodes != null) {
2544+
// Resolve bootnodes: CLI --bootnodes overrides genesis defaults.
2545+
// The discovery protocol version determines the expected format:
2546+
// V5 → ENR strings ("enr:..."), V4 → enode URLs ("enode://...")
2547+
final boolean isV5 =
2548+
unstableNetworkingOptions.toDomainObject().discoveryConfiguration().isDiscoveryV5Enabled();
2549+
List<String> rawBootnodes = null;
2550+
final boolean cliBootnodesProvided = p2PDiscoveryOptions.bootNodes != null;
2551+
if (cliBootnodesProvided) {
25442552
try {
2545-
final List<String> resolvedBootNodeArgs =
2546-
BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes);
2547-
if (!resolvedBootNodeArgs.isEmpty()) {
2548-
if (resolvedBootNodeArgs.getFirst().startsWith("enr:")) {
2549-
builder.setEnrBootNodes(
2550-
resolvedBootNodeArgs.stream().map(EthereumNodeRecord::fromEnr).toList());
2551-
} else {
2552-
listBootNodes = buildEnodes(resolvedBootNodeArgs, getEnodeDnsConfiguration());
2553-
}
2554-
} else {
2555-
listBootNodes = Collections.emptyList();
2556-
}
2557-
2558-
} catch (final BootnodeResolutionException e) {
2553+
rawBootnodes = BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes);
2554+
} catch (final BootnodeResolutionException | IllegalArgumentException e) {
25592555
throw new ParameterException(commandLine, e.getMessage(), e);
2560-
2561-
} catch (final IllegalArgumentException e) {
2562-
throw new ParameterException(commandLine, e.getMessage());
25632556
}
25642557
} else {
2565-
final Optional<List<String>> bootNodesFromGenesis =
2566-
genesisConfigOptionsSupplier.get().getDiscoveryOptions().getBootNodes();
2567-
if (bootNodesFromGenesis.isPresent() && !bootNodesFromGenesis.get().isEmpty()) {
2568-
if (bootNodesFromGenesis.get().getFirst().startsWith("enr:")) {
2569-
builder.setEnrBootNodes(
2570-
bootNodesFromGenesis.get().stream().map(EthereumNodeRecord::fromEnr).toList());
2571-
} else {
2572-
listBootNodes = buildEnodes(bootNodesFromGenesis.get(), getEnodeDnsConfiguration());
2573-
}
2574-
}
2558+
final DiscoveryOptions discoveryOptions =
2559+
genesisConfigOptionsSupplier.get().getDiscoveryOptions();
2560+
rawBootnodes =
2561+
isV5
2562+
? discoveryOptions.getV5BootNodes().orElse(null)
2563+
: discoveryOptions.getBootNodes().orElse(null);
25752564
}
2576-
if (listBootNodes != null) {
2565+
2566+
if (rawBootnodes != null && !rawBootnodes.isEmpty()) {
25772567
if (!p2PDiscoveryOptions.peerDiscoveryEnabled) {
25782568
logger.warn("Discovery disabled: bootnodes will be ignored.");
25792569
}
2580-
DiscoveryConfiguration.assertValidBootnodes(listBootNodes);
2581-
builder.setEnodeBootNodes(listBootNodes);
2570+
try {
2571+
if (isV5) {
2572+
builder.setEnrBootNodes(
2573+
rawBootnodes.stream()
2574+
.map(
2575+
enr -> {
2576+
try {
2577+
return EthereumNodeRecord.fromEnr(enr);
2578+
} catch (final Exception e) {
2579+
throw new ParameterException(
2580+
commandLine,
2581+
"Invalid ENR bootnode: '"
2582+
+ enr
2583+
+ "'. ENR bootnodes must start with 'enr:'. Error: "
2584+
+ e.getMessage(),
2585+
e);
2586+
}
2587+
})
2588+
.toList());
2589+
} else {
2590+
final List<EnodeURLImpl> enodes = buildEnodes(rawBootnodes, getEnodeDnsConfiguration());
2591+
DiscoveryConfiguration.assertValidBootnodes(enodes);
2592+
builder.setEnodeBootNodes(enodes);
2593+
}
2594+
// CLI --bootnodes is a full override: clear the unused protocol's list
2595+
if (cliBootnodesProvided) {
2596+
if (isV5) {
2597+
builder.setEnodeBootNodes(Collections.emptyList());
2598+
} else {
2599+
builder.setEnrBootNodes(Collections.emptyList());
2600+
}
2601+
}
2602+
} catch (final ParameterException e) {
2603+
throw e; // re-throw ParameterException from ENR parsing as-is
2604+
} catch (final IllegalArgumentException e) {
2605+
throw new ParameterException(commandLine, e.getMessage());
2606+
} catch (final RuntimeException e) {
2607+
throw new ParameterException(commandLine, "Invalid bootnode format: " + e.getMessage(), e);
2608+
}
2609+
} else if (cliBootnodesProvided) {
2610+
// Explicitly empty --bootnodes clears all default bootnodes
2611+
builder.setEnodeBootNodes(Collections.emptyList());
2612+
builder.setEnrBootNodes(Collections.emptyList());
25822613
}
25832614
return builder.build();
25842615
}

app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
*/
1515
package org.hyperledger.besu.cli.config;
1616

17+
import org.hyperledger.besu.config.DiscoveryOptions;
1718
import org.hyperledger.besu.config.GenesisConfig;
18-
import org.hyperledger.besu.config.GenesisConfigOptions;
1919
import org.hyperledger.besu.config.NetworkDefinition;
2020
import org.hyperledger.besu.ethereum.p2p.discovery.dns.EthereumNodeRecord;
2121
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl;
@@ -25,10 +25,8 @@
2525
import java.math.BigInteger;
2626
import java.net.URL;
2727
import java.nio.charset.StandardCharsets;
28-
import java.util.ArrayList;
2928
import java.util.List;
3029
import java.util.Objects;
31-
import java.util.Optional;
3230

3331
/**
3432
* The Eth network config.
@@ -72,25 +70,27 @@ public record EthNetworkConfig(
7270
public static EthNetworkConfig getNetworkConfig(final NetworkDefinition networkDefinition) {
7371
final URL genesisSource = jsonConfigSource(networkDefinition.getGenesisFile());
7472
final GenesisConfig genesisConfig = GenesisConfig.fromSource(genesisSource);
75-
final GenesisConfigOptions genesisConfigOptions = genesisConfig.getConfigOptions();
76-
final Optional<List<String>> rawBootNodes =
77-
genesisConfigOptions.getDiscoveryOptions().getBootNodes();
78-
final List<EnodeURLImpl> enodeBootNodes = new ArrayList<>();
79-
final List<EthereumNodeRecord> enrBootNodes = new ArrayList<>();
80-
if (rawBootNodes.isPresent() && !rawBootNodes.get().isEmpty()) {
81-
if (rawBootNodes.get().getFirst().startsWith("enr:")) {
82-
enrBootNodes.addAll(rawBootNodes.get().stream().map(EthereumNodeRecord::fromEnr).toList());
83-
} else {
84-
enodeBootNodes.addAll(rawBootNodes.get().stream().map(EnodeURLImpl::fromString).toList());
85-
}
86-
}
73+
final DiscoveryOptions discoveryOptions =
74+
genesisConfig.getConfigOptions().getDiscoveryOptions();
75+
76+
final List<EnodeURLImpl> enodeBootNodes =
77+
discoveryOptions
78+
.getBootNodes()
79+
.map(nodes -> nodes.stream().map(EnodeURLImpl::fromString).toList())
80+
.orElse(List.of());
81+
82+
final List<EthereumNodeRecord> enrBootNodes =
83+
discoveryOptions
84+
.getV5BootNodes()
85+
.map(nodes -> nodes.stream().map(EthereumNodeRecord::fromEnr).toList())
86+
.orElse(List.of());
8787

8888
return new EthNetworkConfig(
8989
genesisConfig,
9090
networkDefinition.getNetworkId(),
9191
enodeBootNodes,
9292
enrBootNodes,
93-
genesisConfigOptions.getDiscoveryOptions().getDiscoveryDnsUrl().orElse(null));
93+
discoveryOptions.getDiscoveryDnsUrl().orElse(null));
9494
}
9595

9696
private static URL jsonConfigSource(final String resourceName) {

app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
import org.junit.jupiter.api.Test;
114114
import org.junit.jupiter.api.extension.ExtendWith;
115115
import org.junit.jupiter.api.io.TempDir;
116+
import org.junit.jupiter.params.ParameterizedTest;
117+
import org.junit.jupiter.params.provider.ValueSource;
116118
import org.mockito.ArgumentCaptor;
117119
import org.mockito.MockedStatic;
118120
import org.mockito.junit.jupiter.MockitoExtension;
@@ -306,14 +308,7 @@ public void callingBesuCommandWithoutOptionsMustSyncWithDefaultValues() {
306308
final ArgumentCaptor<EthNetworkConfig> ethNetworkArg =
307309
ArgumentCaptor.forClass(EthNetworkConfig.class);
308310
verify(mockRunnerBuilder).discoveryEnabled(eq(true));
309-
verify(mockRunnerBuilder)
310-
.ethNetworkConfig(
311-
new EthNetworkConfig(
312-
GenesisConfig.fromResource(MAINNET.getGenesisFile()),
313-
MAINNET.getNetworkId(),
314-
MAINNET_BOOTSTRAP_NODES,
315-
Collections.emptyList(),
316-
MAINNET_DISCOVERY_URL));
311+
verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET));
317312
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1"));
318313
verify(mockRunnerBuilder).p2pListenPort(eq(30303));
319314
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(DEFAULT_JSON_RPC_CONFIGURATION));
@@ -958,6 +953,7 @@ public void callingWithBootnodesOptionButNoValueMustPassEmptyBootnodeList() {
958953
verify(mockRunnerBuilder).build();
959954

960955
assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty();
956+
assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).isEmpty();
961957

962958
assertThat(commandOutput.toString(UTF_8)).isEmpty();
963959
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
@@ -1020,6 +1016,46 @@ public void callingWithInvalidBootnodeAndEqualSignMustDisplayError() {
10201016
assertThat(commandErrorOutput.toString(UTF_8)).startsWith(expectedErrorOutputStart);
10211017
}
10221018

1019+
private static final String VALID_ENR_1 =
1020+
"enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo";
1021+
private static final String VALID_ENR_2 =
1022+
"enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo";
1023+
1024+
@Test
1025+
public void callingWithValidEnrBootnodeAndV5EnabledMustSucceed() {
1026+
parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1);
1027+
1028+
verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture());
1029+
verify(mockRunnerBuilder).build();
1030+
1031+
assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(1);
1032+
assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty();
1033+
assertThat(commandOutput.toString(UTF_8)).isEmpty();
1034+
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
1035+
}
1036+
1037+
@Test
1038+
public void callingWithMultipleValidEnrBootnodesAndV5EnabledMustSucceed() {
1039+
parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1 + "," + VALID_ENR_2);
1040+
1041+
verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture());
1042+
verify(mockRunnerBuilder).build();
1043+
1044+
assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(2);
1045+
assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty();
1046+
assertThat(commandOutput.toString(UTF_8)).isEmpty();
1047+
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
1048+
}
1049+
1050+
@ParameterizedTest
1051+
@ValueSource(strings = {"enr:-invalidenrdata", "enr:invalidvalue", "invalidvalue"})
1052+
public void callingWithInvalidBootnodeAndV5EnabledMustDisplayError(final String bootnode) {
1053+
parseCommand("--Xv5-discovery-enabled", "--bootnodes", bootnode);
1054+
assertThat(commandOutput.toString(UTF_8)).isEmpty();
1055+
assertThat(commandErrorOutput.toString(UTF_8))
1056+
.contains("Invalid ENR bootnode: '" + bootnode + "'");
1057+
}
1058+
10231059
@Test
10241060
public void bootnodesOptionMustBeUsed() {
10251061
parseCommand("--bootnodes", String.join(",", VALID_ENODE_STRINGS));

app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import static org.hyperledger.besu.config.NetworkDefinition.MAINNET;
2222
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
2323
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3;
24-
import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES;
25-
import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_DISCOVERY_URL;
2624
import static org.mockito.ArgumentMatchers.any;
2725
import static org.mockito.ArgumentMatchers.eq;
2826
import static org.mockito.Mockito.verify;
@@ -132,6 +130,7 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile(final @TempDir File
132130
.setNetworkId(BigInteger.valueOf(42))
133131
.setGenesisConfig(GenesisConfig.fromConfig(encodeJsonGenesis(GENESIS_VALID_JSON)))
134132
.setEnodeBootNodes(nodes)
133+
.setEnrBootNodes(Collections.emptyList())
135134
.setDnsDiscoveryUrl(null)
136135
.build();
137136
verify(mockControllerBuilder).dataDirectory(eq(dataFolder.toPath()));
@@ -165,14 +164,7 @@ public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() {
165164
final MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build();
166165

167166
verify(mockRunnerBuilder).discoveryEnabled(eq(true));
168-
verify(mockRunnerBuilder)
169-
.ethNetworkConfig(
170-
new EthNetworkConfig(
171-
GenesisConfig.fromResource(MAINNET.getGenesisFile()),
172-
MAINNET.getNetworkId(),
173-
MAINNET_BOOTSTRAP_NODES,
174-
Collections.emptyList(),
175-
MAINNET_DISCOVERY_URL));
167+
verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET));
176168
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1"));
177169
verify(mockRunnerBuilder).p2pListenPort(eq(30303));
178170
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration));

app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public void testDefaultMainnetConfig() {
4040
EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET);
4141
assertThat(config.dnsDiscoveryUrl()).isEqualTo(MAINNET_DISCOVERY_URL);
4242
assertThat(config.enodeBootNodes()).isEqualTo(MAINNET_BOOTSTRAP_NODES);
43+
assertThat(config.enrBootNodes()).isNotEmpty();
4344
assertThat(config.networkId()).isEqualTo(BigInteger.ONE);
4445
}
4546

@@ -56,6 +57,7 @@ public void testDefaultHoodiConfig() {
5657
EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.HOODI);
5758
assertThat(config.dnsDiscoveryUrl()).isEqualTo(HOODI_DISCOVERY_URL);
5859
assertThat(config.enodeBootNodes()).isEqualTo(HOODI_BOOTSTRAP_NODES);
60+
assertThat(config.enrBootNodes()).isNotEmpty();
5961
assertThat(config.networkId()).isEqualTo(BigInteger.valueOf(560048));
6062
}
6163

config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class DiscoveryOptions {
2828
new DiscoveryOptions(JsonUtil.createEmptyObjectNode());
2929

3030
private static final String ENODES_KEY = "bootnodes";
31+
private static final String V5_BOOTNODES_KEY = "v5bootnodes";
3132
private static final String DNS_KEY = "dns";
3233

3334
private final ObjectNode discoveryConfigRoot;
@@ -67,6 +68,32 @@ public Optional<List<String>> getBootNodes() {
6768
return Optional.of(bootNodes);
6869
}
6970

71+
/**
72+
* Gets V5 boot nodes (ENR format).
73+
*
74+
* @return optional list of ENR boot node strings
75+
*/
76+
public Optional<List<String>> getV5BootNodes() {
77+
final Optional<ArrayNode> bootNodesArray =
78+
JsonUtil.getArrayNode(discoveryConfigRoot, V5_BOOTNODES_KEY);
79+
if (bootNodesArray.isEmpty()) {
80+
return Optional.empty();
81+
}
82+
final List<String> bootNodes = new ArrayList<>();
83+
bootNodesArray
84+
.get()
85+
.elements()
86+
.forEachRemaining(
87+
bootNodeElement -> {
88+
if (!bootNodeElement.isTextual()) {
89+
throw new IllegalArgumentException(
90+
V5_BOOTNODES_KEY + " does not contain a string: " + bootNodeElement);
91+
}
92+
bootNodes.add(bootNodeElement.asText());
93+
});
94+
return Optional.of(bootNodes);
95+
}
96+
7097
/**
7198
* Gets discovery dns url.
7299
*

0 commit comments

Comments
 (0)