diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java index 4e228441cc4..2098dfeadc1 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java @@ -233,6 +233,11 @@ public String getProvider() { return PROVIDER; } + @Override + public ECDomainParameters getCurve() { + return curve; + } + /** * Gets K calculator. * diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java index a1a79d057a5..1d077d51d26 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java @@ -20,6 +20,7 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.math.ec.ECPoint; /** The interface Signature algorithm. */ @@ -124,6 +125,13 @@ SECPSignature normaliseSignature( */ String getCurveName(); + /** + * Bouncy castle ECDomainParameters representing the curve. + * + * @return instance of ECDomainParameters + */ + ECDomainParameters getCurve(); + /** * Create secp private key. * diff --git a/ethereum/p2p/build.gradle b/ethereum/p2p/build.gradle index 6ed42425e0a..f4eb5066284 100644 --- a/ethereum/p2p/build.gradle +++ b/ethereum/p2p/build.gradle @@ -46,9 +46,6 @@ dependencies { implementation 'io.tmio:tuweni-bytes' implementation 'io.tmio:tuweni-crypto' - implementation('io.tmio:tuweni-devp2p') { - exclude group:'ch.qos.logback', module:'logback-classic' - } implementation 'io.tmio:tuweni-io' implementation 'io.tmio:tuweni-rlp' implementation 'io.tmio:tuweni-units' diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemon.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemon.java index ef794ae2047..7c2d9350f62 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemon.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemon.java @@ -18,7 +18,6 @@ import java.util.Optional; import io.vertx.core.AbstractVerticle; -import org.apache.tuweni.devp2p.EthereumNodeRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonListener.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonListener.java index cfa51d4eb73..81629268a8a 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonListener.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonListener.java @@ -16,8 +16,6 @@ import java.util.List; -import org.apache.tuweni.devp2p.EthereumNodeRecord; - // Adapted from https://github.com/tmio/tuweni and licensed under Apache 2.0 /** Callback listening to updates of the DNS records. */ @FunctionalInterface diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSEntry.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSEntry.java index cd439eea063..810bd3b41b1 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSEntry.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSEntry.java @@ -25,7 +25,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.crypto.SECP256K1; -import org.apache.tuweni.devp2p.EthereumNodeRecord; import org.apache.tuweni.io.Base32; import org.apache.tuweni.io.Base64URLSafe; import org.bouncycastle.math.ec.ECPoint; diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSResolver.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSResolver.java index 0be4ca619d1..c3347aa8d9e 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSResolver.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSResolver.java @@ -33,7 +33,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.crypto.SECP256K1; -import org.apache.tuweni.devp2p.EthereumNodeRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSVisitor.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSVisitor.java index c6ea0a77ed7..fc9b5af9965 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSVisitor.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSVisitor.java @@ -14,8 +14,6 @@ */ package org.hyperledger.besu.ethereum.p2p.discovery.dns; -import org.apache.tuweni.devp2p.EthereumNodeRecord; - // Adapted from https://github.com/tmio/tuweni and licensed under Apache 2.0 /** * Reads ENR (Ethereum Node Records) entries passed in from DNS. The visitor may decide to stop the 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 new file mode 100644 index 00000000000..339afcffca3 --- /dev/null +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecord.java @@ -0,0 +1,154 @@ +/* + * Copyright contributors to Hyperledger 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 + */ + +// 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.ethereum.rlp.BytesValueRLPInput; + +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; + +/** + * A modified implementation of Ethereum Node Record (ENR) that is used by DNSResolver. See EIP-778 + */ +public record EthereumNodeRecord( + Bytes rlp, Bytes publicKey, InetAddress ip, Optional tcp, Optional udp) { + + /** + * Creates an ENR from its serialized form as a RLP list + * + * @param rlp the serialized form of the ENR + * @return the ENR + * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes + */ + public static EthereumNodeRecord fromRLP(final Bytes rlp) { + if (rlp.size() > 300) { + throw new IllegalArgumentException("Record too long"); + } + var data = new HashMap(); + + // rlp: sig, sequence, k1,v1, k2,v2, k3, [v3, vn]... + var input = new BytesValueRLPInput(rlp, false); + input.enterList(); + + input.skipNext(); // skip signature + input.skipNext(); // skip sequence + + // go through rest of the list + while (!input.isEndOfCurrentList()) { + var key = new String(input.readBytes().toArrayUnsafe(), StandardCharsets.UTF_8); + if (input.nextIsList()) { + // skip list as we currently don't need any of these complex structures + input.skipNext(); + } else { + data.put(key, input.readBytes()); + } + } + + input.leaveList(); + + var publicKey = initPublicKeyBytes(data); + + return new EthereumNodeRecord(rlp, publicKey, initIPAddr(data), initTCP(data), initUDP(data)); + } + + /** + * Returns the public key of the ENR + * + * @return the public key of the ENR + */ + static Bytes initPublicKeyBytes(final Map data) { + var keyBytes = data.get("secp256k1"); + if (keyBytes == null) { + 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()); + // uncompressed public key is 65 bytes, first byte is 0x04. + var encodedPubKey = ecPoint.getEncoded(false); + return Bytes.of(Arrays.copyOfRange(encodedPubKey, 1, encodedPubKey.length)); + } + + /** + * Returns the InetAddress of the ENR + * + * @return The IP address of the ENR + */ + static InetAddress initIPAddr(final Map data) { + var ipBytes = data.get("ip"); + if (ipBytes != null) { + try { + return InetAddress.getByAddress(ipBytes.toArrayUnsafe()); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + return InetAddress.getLoopbackAddress(); + } + + /** + * The TCP port of the ENR + * + * @return the TCP port associated with this ENR + */ + static Optional initTCP(final Map data) { + var tcpBytes = data.get("tcp"); + return tcpBytes != null ? Optional.of(tcpBytes.toInt()) : Optional.empty(); + } + + /** + * The UDP port of the ENR. If the UDP port is not present, the TCP port is used. + * + * @return the UDP port associated with this ENR + */ + static Optional initUDP(final Map data) { + var udpBytes = data.get("udp"); + return udpBytes != null ? Optional.of(udpBytes.toInt()) : initTCP(data); + } + + /** + * @return the ENR as a URI + */ + @Override + public String toString() { + return "enr:" + ip() + ":" + tcp() + "?udp=" + udp(); + } + + /** Override equals method to compare the RLP bytes */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof EthereumNodeRecord that)) { + return false; + } + return Objects.equals(rlp, that.rlp); + } + + /** Override hashCode method to use hashCode of the RLP bytes */ + @Override + public int hashCode() { + return Objects.hashCode(rlp); + } +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java index 610ebd39d8a..bf8e76b6215 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.ethereum.p2p.discovery.VertxPeerDiscoveryAgent; import org.hyperledger.besu.ethereum.p2p.discovery.dns.DNSDaemon; import org.hyperledger.besu.ethereum.p2p.discovery.dns.DNSDaemonListener; +import org.hyperledger.besu.ethereum.p2p.discovery.dns.EthereumNodeRecord; import org.hyperledger.besu.ethereum.p2p.discovery.internal.PeerTable; import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeerPrivileges; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; @@ -82,7 +83,6 @@ import io.vertx.core.ThreadingModel; import io.vertx.core.Vertx; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.devp2p.EthereumNodeRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -366,9 +366,9 @@ DNSDaemonListener createDaemonListener() { final EnodeURL enodeURL = EnodeURLImpl.builder() .ipAddress(enr.ip()) - .nodeId(enr.publicKey().bytes()) - .discoveryPort(Optional.ofNullable(enr.udp())) - .listeningPort(Optional.ofNullable(enr.tcp())) + .nodeId(enr.publicKey()) + .discoveryPort(enr.udp()) + .listeningPort(enr.tcp()) .build(); final DiscoveryPeer peer = DiscoveryPeer.fromEnode(enodeURL); peers.add(peer); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonTest.java index 94d9c75e4ae..b25148126c4 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/DNSDaemonTest.java @@ -14,6 +14,8 @@ */ package org.hyperledger.besu.ethereum.p2p.discovery.dns; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; + import java.security.Security; import java.util.concurrent.atomic.AtomicInteger; @@ -67,10 +69,24 @@ void testDNSDaemon(final Vertx vertx, final VertxTestContext testContext) { testContext.failNow( "Expecting 115 records in first pass but got: " + records.size()); } + records.forEach( + enr -> { + try { + // make sure enode url can be built from record + EnodeURLImpl.builder() + .ipAddress(enr.ip()) + .nodeId(enr.publicKey()) + .discoveryPort(enr.udp()) + .listeningPort(enr.tcp()) + .build(); + } catch (final Exception e) { + testContext.failNow(e); + } + }); checkpoint.flag(); }, 0, - 0, + 1L, 0, "localhost:" + mockDnsServerVerticle.port()); diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecordTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecordTest.java new file mode 100644 index 00000000000..da5cfd0f31c --- /dev/null +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/dns/EthereumNodeRecordTest.java @@ -0,0 +1,56 @@ +/* + * 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.p2p.discovery.dns; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.InetAddress; +import java.util.Random; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +class EthereumNodeRecordTest { + + @Test + void buildFromRLP() throws Exception { + final Bytes rlp = + Bytes.fromHexString( + "0xf8a3b84033b8a07e5c8e19dc8ac2529354b21a6c09e5516335eb57c383924aa0ca73434c0c65d8625eb05236e172fcc00d80e913506bde5446fb5c55ea2035380c97480a86018d56dc241083657468c7c6849b192ad0808269648276348269708441157e4389736563703235366b31a102a48c4c032f4c2e1b4007dd15b0d7046b60774f6bc38e2f52a8e0361c65e4234284736e6170c08374637082765f8375647082765f"); + // method under test + final EthereumNodeRecord enr = EthereumNodeRecord.fromRLP(rlp); + // expected values + final InetAddress expectedIPAddr = + InetAddress.getByAddress(Bytes.fromHexString("0x41157e43").toArrayUnsafe()); + final Bytes expectedPublicKey = + Bytes.fromHexString( + "0xa48c4c032f4c2e1b4007dd15b0d7046b60774f6bc38e2f52a8e0361c65e423424520b07898c59a8c9e85c440594ca734f23b7f2b906d5da54676eee6a1d64874"); + + // assertions + assertThat(enr.ip()).isEqualTo(expectedIPAddr); + assertThat(enr.publicKey()).isEqualTo(expectedPublicKey); + assertThat(enr.tcp()).isNotEmpty().contains(30303); + assertThat(enr.udp()).isNotEmpty().contains(30303); + } + + @Test + void buildFromRLPWithSizeGreaterThan300() { + final Bytes rlp = Bytes.random(301, new Random(1L)); + assertThatThrownBy(() -> EthereumNodeRecord.fromRLP(rlp)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Record too long"); + } +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c3acee03312..1a36b557aa2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2883,14 +2883,6 @@ - - - - - - - - diff --git a/platform/build.gradle b/platform/build.gradle index f5bb54afcd1..cffc33659ff 100644 --- a/platform/build.gradle +++ b/platform/build.gradle @@ -110,7 +110,6 @@ dependencies { api 'io.tmio:tuweni-config:2.4.2' api 'io.tmio:tuweni-concurrent:2.4.2' api 'io.tmio:tuweni-crypto:2.4.2' - api 'io.tmio:tuweni-devp2p:2.4.2' api 'io.tmio:tuweni-io:2.4.2' api 'io.tmio:tuweni-net:2.4.2' api 'io.tmio:tuweni-rlp:2.4.2'