diff --git a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java index d0beb616401..0901658cd24 100644 --- a/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/app/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -22,7 +22,6 @@ import org.hyperledger.besu.cli.config.EthNetworkConfig; import org.hyperledger.besu.cli.options.EthstatsOptions; -import org.hyperledger.besu.config.NetworkDefinition; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.cryptoservices.NodeKey; import org.hyperledger.besu.ethereum.ProtocolContext; @@ -672,17 +671,8 @@ public Runner build() { }); discoveryConfiguration.setPreferIpv6Outbound(preferIpv6Outbound); if (discoveryEnabled) { - final List bootstrap; - if (ethNetworkConfig.enodeBootNodes() == null) { - bootstrap = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enodeBootNodes(); - } else { - bootstrap = ethNetworkConfig.enodeBootNodes(); - } - discoveryConfiguration.setEnodeBootnodes(bootstrap); - discoveryConfiguration.setEnrBootnodes( - ethNetworkConfig.enrBootNodes() == null - ? EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET).enrBootNodes() - : ethNetworkConfig.enrBootNodes()); + discoveryConfiguration.setEnodeBootnodes(ethNetworkConfig.enodeBootNodes()); + discoveryConfiguration.setEnrBootnodes(ethNetworkConfig.enrBootNodes()); discoveryConfiguration.setIncludeBootnodesOnPeerRefresh( besuController.getGenesisConfigOptions().isPoa() && poaDiscoveryRetryBootnodes); @@ -690,7 +680,10 @@ public Runner build() { "Resolved {} bootnodes.", discoveryConfiguration.getEnodeBootnodes().size() + discoveryConfiguration.getEnrBootnodes().size()); - LOG.debug("Bootnodes = {}", bootstrap); + LOG.debug( + "Bootnodes enode={}, enr={}", + discoveryConfiguration.getEnodeBootnodes(), + discoveryConfiguration.getEnrBootnodes()); discoveryConfiguration.setDnsDiscoveryURL(ethNetworkConfig.dnsDiscoveryUrl()); discoveryConfiguration.setDiscoveryV5Enabled( networkingConfiguration.discoveryConfiguration().isDiscoveryV5Enabled()); diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 747c2e92cb0..354386f0491 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -88,6 +88,7 @@ import org.hyperledger.besu.cli.util.VersionProvider; import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.CheckpointConfigOptions; +import org.hyperledger.besu.config.DiscoveryOptions; import org.hyperledger.besu.config.GenesisConfig; import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.JsonUtil; @@ -2517,6 +2518,7 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) { if (p2PDiscoveryOptions.bootNodes == null) { builder.setEnodeBootNodes(new ArrayList<>()); + builder.setEnrBootNodes(new ArrayList<>()); } builder.setDnsDiscoveryUrl(null); } @@ -2539,46 +2541,75 @@ private EthNetworkConfig updateNetworkConfig(final NetworkDefinition network) { discoveryDnsUrlFromGenesis.ifPresent(builder::setDnsDiscoveryUrl); } - List listBootNodes = null; - if (p2PDiscoveryOptions.bootNodes != null) { + // Resolve bootnodes: CLI --bootnodes overrides genesis defaults. + // The discovery protocol version determines the expected format: + // V5 → ENR strings ("enr:..."), V4 → enode URLs ("enode://...") + final boolean isV5 = + unstableNetworkingOptions.toDomainObject().discoveryConfiguration().isDiscoveryV5Enabled(); + List rawBootnodes = null; + final boolean cliBootnodesProvided = p2PDiscoveryOptions.bootNodes != null; + if (cliBootnodesProvided) { try { - final List resolvedBootNodeArgs = - BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes); - if (!resolvedBootNodeArgs.isEmpty()) { - if (resolvedBootNodeArgs.getFirst().startsWith("enr:")) { - builder.setEnrBootNodes( - resolvedBootNodeArgs.stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - listBootNodes = buildEnodes(resolvedBootNodeArgs, getEnodeDnsConfiguration()); - } - } else { - listBootNodes = Collections.emptyList(); - } - - } catch (final BootnodeResolutionException e) { + rawBootnodes = BootnodeResolver.resolve(p2PDiscoveryOptions.bootNodes); + } catch (final BootnodeResolutionException | IllegalArgumentException e) { throw new ParameterException(commandLine, e.getMessage(), e); - - } catch (final IllegalArgumentException e) { - throw new ParameterException(commandLine, e.getMessage()); } } else { - final Optional> bootNodesFromGenesis = - genesisConfigOptionsSupplier.get().getDiscoveryOptions().getBootNodes(); - if (bootNodesFromGenesis.isPresent() && !bootNodesFromGenesis.get().isEmpty()) { - if (bootNodesFromGenesis.get().getFirst().startsWith("enr:")) { - builder.setEnrBootNodes( - bootNodesFromGenesis.get().stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - listBootNodes = buildEnodes(bootNodesFromGenesis.get(), getEnodeDnsConfiguration()); - } - } + final DiscoveryOptions discoveryOptions = + genesisConfigOptionsSupplier.get().getDiscoveryOptions(); + rawBootnodes = + isV5 + ? discoveryOptions.getV5BootNodes().orElse(null) + : discoveryOptions.getBootNodes().orElse(null); } - if (listBootNodes != null) { + + if (rawBootnodes != null && !rawBootnodes.isEmpty()) { if (!p2PDiscoveryOptions.peerDiscoveryEnabled) { logger.warn("Discovery disabled: bootnodes will be ignored."); } - DiscoveryConfiguration.assertValidBootnodes(listBootNodes); - builder.setEnodeBootNodes(listBootNodes); + try { + if (isV5) { + builder.setEnrBootNodes( + rawBootnodes.stream() + .map( + enr -> { + try { + return EthereumNodeRecord.fromEnr(enr); + } catch (final Exception e) { + throw new ParameterException( + commandLine, + "Invalid ENR bootnode: '" + + enr + + "'. ENR bootnodes must start with 'enr:'. Error: " + + e.getMessage(), + e); + } + }) + .toList()); + } else { + final List enodes = buildEnodes(rawBootnodes, getEnodeDnsConfiguration()); + DiscoveryConfiguration.assertValidBootnodes(enodes); + builder.setEnodeBootNodes(enodes); + } + // CLI --bootnodes is a full override: clear the unused protocol's list + if (cliBootnodesProvided) { + if (isV5) { + builder.setEnodeBootNodes(Collections.emptyList()); + } else { + builder.setEnrBootNodes(Collections.emptyList()); + } + } + } catch (final ParameterException e) { + throw e; // re-throw ParameterException from ENR parsing as-is + } catch (final IllegalArgumentException e) { + throw new ParameterException(commandLine, e.getMessage()); + } catch (final RuntimeException e) { + throw new ParameterException(commandLine, "Invalid bootnode format: " + e.getMessage(), e); + } + } else if (cliBootnodesProvided) { + // Explicitly empty --bootnodes clears all default bootnodes + builder.setEnodeBootNodes(Collections.emptyList()); + builder.setEnrBootNodes(Collections.emptyList()); } return builder.build(); } diff --git a/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java b/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java index 7a34f44fcb4..9845b727c37 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java +++ b/app/src/main/java/org/hyperledger/besu/cli/config/EthNetworkConfig.java @@ -14,8 +14,8 @@ */ package org.hyperledger.besu.cli.config; +import org.hyperledger.besu.config.DiscoveryOptions; import org.hyperledger.besu.config.GenesisConfig; -import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.NetworkDefinition; import org.hyperledger.besu.ethereum.p2p.discovery.dns.EthereumNodeRecord; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; @@ -25,10 +25,8 @@ import java.math.BigInteger; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; /** * The Eth network config. @@ -72,25 +70,27 @@ public record EthNetworkConfig( public static EthNetworkConfig getNetworkConfig(final NetworkDefinition networkDefinition) { final URL genesisSource = jsonConfigSource(networkDefinition.getGenesisFile()); final GenesisConfig genesisConfig = GenesisConfig.fromSource(genesisSource); - final GenesisConfigOptions genesisConfigOptions = genesisConfig.getConfigOptions(); - final Optional> rawBootNodes = - genesisConfigOptions.getDiscoveryOptions().getBootNodes(); - final List enodeBootNodes = new ArrayList<>(); - final List enrBootNodes = new ArrayList<>(); - if (rawBootNodes.isPresent() && !rawBootNodes.get().isEmpty()) { - if (rawBootNodes.get().getFirst().startsWith("enr:")) { - enrBootNodes.addAll(rawBootNodes.get().stream().map(EthereumNodeRecord::fromEnr).toList()); - } else { - enodeBootNodes.addAll(rawBootNodes.get().stream().map(EnodeURLImpl::fromString).toList()); - } - } + final DiscoveryOptions discoveryOptions = + genesisConfig.getConfigOptions().getDiscoveryOptions(); + + final List enodeBootNodes = + discoveryOptions + .getBootNodes() + .map(nodes -> nodes.stream().map(EnodeURLImpl::fromString).toList()) + .orElse(List.of()); + + final List enrBootNodes = + discoveryOptions + .getV5BootNodes() + .map(nodes -> nodes.stream().map(EthereumNodeRecord::fromEnr).toList()) + .orElse(List.of()); return new EthNetworkConfig( genesisConfig, networkDefinition.getNetworkId(), enodeBootNodes, enrBootNodes, - genesisConfigOptions.getDiscoveryOptions().getDiscoveryDnsUrl().orElse(null)); + discoveryOptions.getDiscoveryDnsUrl().orElse(null)); } private static URL jsonConfigSource(final String resourceName) { diff --git a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index db3e7740abb..b4132438cfc 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -113,6 +113,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; @@ -306,14 +308,7 @@ public void callingBesuCommandWithoutOptionsMustSyncWithDefaultValues() { final ArgumentCaptor ethNetworkArg = ArgumentCaptor.forClass(EthNetworkConfig.class); verify(mockRunnerBuilder).discoveryEnabled(eq(true)); - verify(mockRunnerBuilder) - .ethNetworkConfig( - new EthNetworkConfig( - GenesisConfig.fromResource(MAINNET.getGenesisFile()), - MAINNET.getNetworkId(), - MAINNET_BOOTSTRAP_NODES, - Collections.emptyList(), - MAINNET_DISCOVERY_URL)); + verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET)); verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1")); verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(DEFAULT_JSON_RPC_CONFIGURATION)); @@ -958,6 +953,7 @@ public void callingWithBootnodesOptionButNoValueMustPassEmptyBootnodeList() { verify(mockRunnerBuilder).build(); assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).isEmpty(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); @@ -1020,6 +1016,46 @@ public void callingWithInvalidBootnodeAndEqualSignMustDisplayError() { assertThat(commandErrorOutput.toString(UTF_8)).startsWith(expectedErrorOutputStart); } + private static final String VALID_ENR_1 = + "enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo"; + private static final String VALID_ENR_2 = + "enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo"; + + @Test + public void callingWithValidEnrBootnodeAndV5EnabledMustSucceed() { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1); + + verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(1); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void callingWithMultipleValidEnrBootnodesAndV5EnabledMustSucceed() { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", VALID_ENR_1 + "," + VALID_ENR_2); + + verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(ethNetworkConfigArgumentCaptor.getValue().enrBootNodes()).hasSize(2); + assertThat(ethNetworkConfigArgumentCaptor.getValue().enodeBootNodes()).isEmpty(); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"enr:-invalidenrdata", "enr:invalidvalue", "invalidvalue"}) + public void callingWithInvalidBootnodeAndV5EnabledMustDisplayError(final String bootnode) { + parseCommand("--Xv5-discovery-enabled", "--bootnodes", bootnode); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Invalid ENR bootnode: '" + bootnode + "'"); + } + @Test public void bootnodesOptionMustBeUsed() { parseCommand("--bootnodes", String.join(",", VALID_ENODE_STRINGS)); diff --git a/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java b/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java index 9fdeed43a37..28cc78a0907 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/CascadingDefaultProviderTest.java @@ -21,8 +21,6 @@ import static org.hyperledger.besu.config.NetworkDefinition.MAINNET; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3; -import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES; -import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_DISCOVERY_URL; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -132,6 +130,7 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile(final @TempDir File .setNetworkId(BigInteger.valueOf(42)) .setGenesisConfig(GenesisConfig.fromConfig(encodeJsonGenesis(GENESIS_VALID_JSON))) .setEnodeBootNodes(nodes) + .setEnrBootNodes(Collections.emptyList()) .setDnsDiscoveryUrl(null) .build(); verify(mockControllerBuilder).dataDirectory(eq(dataFolder.toPath())); @@ -165,14 +164,7 @@ public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() { final MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build(); verify(mockRunnerBuilder).discoveryEnabled(eq(true)); - verify(mockRunnerBuilder) - .ethNetworkConfig( - new EthNetworkConfig( - GenesisConfig.fromResource(MAINNET.getGenesisFile()), - MAINNET.getNetworkId(), - MAINNET_BOOTSTRAP_NODES, - Collections.emptyList(), - MAINNET_DISCOVERY_URL)); + verify(mockRunnerBuilder).ethNetworkConfig(EthNetworkConfig.getNetworkConfig(MAINNET)); verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1")); verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); diff --git a/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java b/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java index 79990d5df4d..0ecac5a6846 100644 --- a/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java +++ b/app/src/test/java/org/hyperledger/besu/cli/config/EthNetworkConfigTest.java @@ -40,6 +40,7 @@ public void testDefaultMainnetConfig() { EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.MAINNET); assertThat(config.dnsDiscoveryUrl()).isEqualTo(MAINNET_DISCOVERY_URL); assertThat(config.enodeBootNodes()).isEqualTo(MAINNET_BOOTSTRAP_NODES); + assertThat(config.enrBootNodes()).isNotEmpty(); assertThat(config.networkId()).isEqualTo(BigInteger.ONE); } @@ -56,6 +57,7 @@ public void testDefaultHoodiConfig() { EthNetworkConfig config = EthNetworkConfig.getNetworkConfig(NetworkDefinition.HOODI); assertThat(config.dnsDiscoveryUrl()).isEqualTo(HOODI_DISCOVERY_URL); assertThat(config.enodeBootNodes()).isEqualTo(HOODI_BOOTSTRAP_NODES); + assertThat(config.enrBootNodes()).isNotEmpty(); assertThat(config.networkId()).isEqualTo(BigInteger.valueOf(560048)); } diff --git a/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java b/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java index 7347615f3f3..c4aae63f7e6 100644 --- a/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/DiscoveryOptions.java @@ -28,6 +28,7 @@ public class DiscoveryOptions { new DiscoveryOptions(JsonUtil.createEmptyObjectNode()); private static final String ENODES_KEY = "bootnodes"; + private static final String V5_BOOTNODES_KEY = "v5bootnodes"; private static final String DNS_KEY = "dns"; private final ObjectNode discoveryConfigRoot; @@ -67,6 +68,32 @@ public Optional> getBootNodes() { return Optional.of(bootNodes); } + /** + * Gets V5 boot nodes (ENR format). + * + * @return optional list of ENR boot node strings + */ + public Optional> getV5BootNodes() { + final Optional bootNodesArray = + JsonUtil.getArrayNode(discoveryConfigRoot, V5_BOOTNODES_KEY); + if (bootNodesArray.isEmpty()) { + return Optional.empty(); + } + final List bootNodes = new ArrayList<>(); + bootNodesArray + .get() + .elements() + .forEachRemaining( + bootNodeElement -> { + if (!bootNodeElement.isTextual()) { + throw new IllegalArgumentException( + V5_BOOTNODES_KEY + " does not contain a string: " + bootNodeElement); + } + bootNodes.add(bootNodeElement.asText()); + }); + return Optional.of(bootNodes); + } + /** * Gets discovery dns url. * diff --git a/config/src/main/resources/hoodi.json b/config/src/main/resources/hoodi.json index b9e435bae6c..18932b25fed 100644 --- a/config/src/main/resources/hoodi.json +++ b/config/src/main/resources/hoodi.json @@ -52,6 +52,17 @@ "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303" + ], + "v5Bootnodes": [ + "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", + "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", + "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", + "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", + "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", + "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", + "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" ] } }, diff --git a/config/src/main/resources/mainnet.json b/config/src/main/resources/mainnet.json index 3ba94ccf767..46695f6fe10 100644 --- a/config/src/main/resources/mainnet.json +++ b/config/src/main/resources/mainnet.json @@ -54,6 +54,25 @@ "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303", "enode://2b252ab6a1d0f971d9722cb839a42cb81db019ba44c08754628ab4a823487071b5695317c8ccd085219c3a03af063495b2f1da8d18218da2d6a82981b45e6ffc@65.108.70.101:30303", "enode://4aeb4ab6c14b23e2c4cfdce879c04b0748a20d8e9b59e25ded2a08143e265c6c25936e74cbc8e641e3312ca288673d91f2f93f8e277de3cfa444ecdaaf982052@157.90.35.166:30303" + ], + "v5Bootnodes": [ + "enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo", + "enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", + "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", + "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", + "enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I", + "enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I", + "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", + "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", + "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", + "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", + "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", + "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", + "enr:-IS4QPi-onjNsT5xAIAenhCGTDl4z-4UOR25Uq-3TmG4V3kwB9ljLTb_Kp1wdjHNj-H8VVLRBSSWVZo3GUe3z6k0E-IBgmlkgnY0gmlwhKB3_qGJc2VjcDI1NmsxoQMvAfgB4cJXvvXeM6WbCG86CstbSxbQBSGx31FAwVtOTYN1ZHCCIyg", + "enr:-KG4QPUf8-g_jU-KrwzG42AGt0wWM1BTnQxgZXlvCEIfTQ5hSmptkmgmMbRkpOqv6kzb33SlhPHJp7x4rLWWiVq5lSECgmlkgnY0gmlwhFPlR9KDaXA2kCoGxcAJAAAVAAAAAAAAABCJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA" ] }, "checkpoint": { diff --git a/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java b/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java new file mode 100644 index 00000000000..05ef67f4696 --- /dev/null +++ b/config/src/test/java/org/hyperledger/besu/config/DiscoveryOptionsTest.java @@ -0,0 +1,109 @@ +/* + * 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.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; + +class DiscoveryOptionsTest { + + @Test + void defaultHasNoBootNodes() { + assertThat(DiscoveryOptions.DEFAULT.getBootNodes()).isEmpty(); + } + + @Test + void defaultHasNoV5BootNodes() { + assertThat(DiscoveryOptions.DEFAULT.getV5BootNodes()).isEmpty(); + } + + @Test + void defaultHasNoDnsUrl() { + assertThat(DiscoveryOptions.DEFAULT.getDiscoveryDnsUrl()).isEmpty(); + } + + @Test + void parsesBootNodes() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + final ArrayNode bootnodes = root.putArray("bootnodes"); + bootnodes.add("enode://abc@127.0.0.1:30303"); + bootnodes.add("enode://def@127.0.0.1:30304"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getBootNodes()) + .isPresent() + .hasValue(List.of("enode://abc@127.0.0.1:30303", "enode://def@127.0.0.1:30304")); + } + + @Test + void parsesV5BootNodes() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + final ArrayNode v5Bootnodes = root.putArray("v5bootnodes"); + v5Bootnodes.add("enr:-abc123"); + v5Bootnodes.add("enr:-def456"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getV5BootNodes()) + .isPresent() + .hasValue(List.of("enr:-abc123", "enr:-def456")); + } + + @Test + void v5BootNodesEmptyWhenKeyAbsent() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("bootnodes").add("enode://abc@127.0.0.1:30303"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getV5BootNodes()).isEmpty(); + assertThat(options.getBootNodes()).isPresent(); + } + + @Test + void bootNodesAndV5BootNodesCanCoexist() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("bootnodes").add("enode://abc@127.0.0.1:30303"); + root.putArray("v5bootnodes").add("enr:-abc123"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getBootNodes()).isPresent().hasValue(List.of("enode://abc@127.0.0.1:30303")); + assertThat(options.getV5BootNodes()).isPresent().hasValue(List.of("enr:-abc123")); + } + + @Test + void v5BootNodesThrowsOnNonStringElement() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.putArray("v5bootnodes").add(42); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThatThrownBy(options::getV5BootNodes) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("v5bootnodes does not contain a string"); + } + + @Test + void parsesDnsUrl() { + final ObjectNode root = JsonUtil.createEmptyObjectNode(); + root.put("dns", "enrtree://test@domain"); + + final DiscoveryOptions options = new DiscoveryOptions(root); + assertThat(options.getDiscoveryDnsUrl()).isPresent().hasValue("enrtree://test@domain"); + } +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java index dd0f18d4f62..ce941313ba7 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/config/DiscoveryConfiguration.java @@ -110,7 +110,7 @@ public DiscoveryConfiguration setEnrBootnodes(final List enr } public List getBootnodeIdentifiers() { - return enodeBootnodes.isEmpty() ? enrBootnodes : enodeBootnodes; + return discoveryV5Enabled ? enrBootnodes : enodeBootnodes; } public boolean getIncludeBootnodesOnPeerRefresh() { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java index ee02baa272a..68529c7963a 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentFactoryV5.java @@ -169,8 +169,21 @@ public NodeKeySigner(final NodeKey nodeKey) { */ @Override public Bytes deriveECDHKeyAgreement(final Bytes remotePubKey) { - SECPPublicKey publicKey = signatureAlgorithm.createPublicKey(remotePubKey); - return nodeKey.calculateECDHKeyAgreement(publicKey); + final Bytes uncompressedKey; + if (remotePubKey.size() == 33) { + // Compressed key (0x02/0x03 prefix) — decompress to the 64-byte format without prefix + final byte[] encoded = + signatureAlgorithm + .getCurve() + .getCurve() + .decodePoint(remotePubKey.toArrayUnsafe()) + .getEncoded(false); + uncompressedKey = Bytes.wrap(encoded, 1, 64); + } else { + uncompressedKey = remotePubKey; + } + final SECPPublicKey publicKey = signatureAlgorithm.createPublicKey(uncompressedKey); + return nodeKey.calculateECDHKeyAgreementCompressed(publicKey); } /** diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java index c4dbe2eb509..659909e7da3 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/discv5/PeerDiscoveryAgentV5.java @@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes; import org.ethereum.beacon.discovery.MutableDiscoverySystem; import org.ethereum.beacon.discovery.schema.NodeRecord; import org.ethereum.beacon.discovery.storage.NodeRecordListener; @@ -445,13 +446,20 @@ private Stream candidatePeers(final Collection newPee return Stream.empty(); } + final Bytes localNodeId = getLocalNodeRecord().map(NodeRecord::getNodeId).orElse(Bytes.EMPTY); + // Combine newly discovered peers with known peers and filter for suitability final Stream knownPeers = system.streamLiveNodes(); final List candidates = Stream.concat(newPeers.stream(), knownPeers) .distinct() + // Exclude the local node record that streamLiveNodes may include + .filter(nr -> !nr.getNodeId().equals(localNodeId)) .map(nr -> DiscoveryPeerFactory.fromNodeRecord(nr, preferIpv6Outbound)) - .filter(DiscoveryPeer::isReadyForConnections) + // Use isListening() instead of isReadyForConnections() because + // DiscoveryPeerV4.isReadyForConnections() requires DiscV4 bonding status, + // which is never set for DiscV5-discovered peers. + .filter(DiscoveryPeer::isListening) .filter(peer -> peer.getForkId().map(forkIdManager::peerCheck).orElse(true)) .toList(); if (LOG.isTraceEnabled() && !candidates.isEmpty()) { diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java index 49518751267..9c03a7890df 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java @@ -16,7 +16,8 @@ // Adapted from https://github.com/tmio/tuweni and licensed under Apache 2.0 package org.hyperledger.besu.ethereum.p2p.discovery.dns; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmType; import org.hyperledger.besu.ethereum.p2p.discovery.NodeIdentifier; import java.net.InetAddress; @@ -46,6 +47,10 @@ public record EthereumNodeRecord( NodeRecord nodeRecord) implements NodeIdentifier { + // ENR identity scheme v4 always uses secp256k1, independent of the node's signature algorithm + private static final SignatureAlgorithm SECP256K1 = + SignatureAlgorithmType.create("secp256k1").getInstance(); + @SuppressWarnings( "MethodInputParametersMustBeFinal") // needed since record constructors are not yet supported public EthereumNodeRecord { @@ -97,8 +102,8 @@ static Bytes initPublicKeyBytes(final Map fields) { throw new IllegalArgumentException("Missing secp256k1 entry in ENR"); } // convert 33 bytes compressed public key to uncompressed using Bouncy Castle - var curve = SignatureAlgorithmFactory.getInstance().getCurve(); - var ecPoint = curve.getCurve().decodePoint(keyBytes.toArrayUnsafe()); + // ENR identity scheme v4 always uses secp256k1 for the public key + var ecPoint = SECP256K1.getCurve().getCurve().decodePoint(keyBytes.toArrayUnsafe()); // uncompressed public key is 65 bytes, first byte is 0x04. var encodedPubKey = ecPoint.getEncoded(false); return Bytes.of(Arrays.copyOfRange(encodedPubKey, 1, encodedPubKey.length));