You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2020/05/02 22:41:42 UTC

[incubator-tuweni] branch master updated: Implement DNS discovery reader, reading from goerli and mainnet

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git


The following commit(s) were added to refs/heads/master by this push:
     new 6e2fcd5  Implement DNS discovery reader, reading from goerli and mainnet
6e2fcd5 is described below

commit 6e2fcd52eda7fe8096a6eb1d04efbafbc555febf
Author: Antoine Toulme <an...@lunar-ocean.com>
AuthorDate: Sat May 2 15:39:58 2020 -0700

    Implement DNS discovery reader, reading from goerli and mainnet
---
 dependency-versions.gradle                         |   1 +
 .../org/apache/tuweni/devp2p/EthereumNodeRecord.kt |  36 ++--
 dns-discovery/build.gradle                         |   1 +
 .../kotlin/org/apache/tuweni/discovery/DNSEntry.kt |  89 +++++++---
 .../org/apache/tuweni/discovery/DNSResolver.kt     | 195 +++++++++++++++++++++
 .../org/apache/tuweni/discovery/DNSVisitor.kt      |  34 ++++
 .../org/apache/tuweni/discovery/DNSEntryTest.kt    |  37 ++--
 .../org/apache/tuweni/discovery/DNSResolverTest.kt |  65 +++++++
 8 files changed, 401 insertions(+), 57 deletions(-)

diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index 3afb555..29299d7 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -12,6 +12,7 @@
  */
 dependencyManagement {
   dependencies {
+    dependency('ch.qos.logback:logback-classic:1.2.3')
     dependency('commons-codec:commons-codec:1.14')
     dependency('com.fasterxml.jackson.core:jackson-databind:2.9.5')
     dependency('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.8')
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
index 61743a0..cc6f7f7 100644
--- a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
@@ -46,20 +46,26 @@ class EthereumNodeRecord(val signature: Bytes, val seq: Long, val data: Map<Stri
       if (rlp.size() > 300) {
         throw IllegalArgumentException("Record too long")
       }
-      return RLP.decodeList(rlp, {
-        val sig = it.readValue()
-
-        val seq = it.readLong()
-
-        val data = mutableMapOf<String, Bytes>()
-        while (!it.isComplete) {
-          val key = it.readString()
-          val value = it.readValue()
-          data[key] = value
-        }
+      return RLP.decodeList(rlp) {
+          val sig = it.readValue()
+
+          val seq = it.readLong()
+
+          val data = mutableMapOf<String, Bytes>()
+          while (!it.isComplete) {
+            val key = it.readString()
+            if (it.nextIsList()) {
+              it.skipNext()
+              // TODO("not ready yet to read list values")
+              // data[key] = it.readListContents { listreader -> listreader.readValue()}
+            } else {
+              val value = it.readValue()
+              data[key] = value
+            }
+          }
 
-        EthereumNodeRecord(sig, seq, data)
-      })
+          EthereumNodeRecord(sig, seq, data)
+      }
     }
 
     private fun encode(
@@ -168,6 +174,10 @@ class EthereumNodeRecord(val signature: Bytes, val seq: Long, val data: Map<Stri
   fun udp(): Int {
     return data["udp"]!!.toInt()
   }
+
+  override fun toString(): String {
+    return "enr:${ip()}:${tcp()}?udp=${udp()}"
+  }
 }
 
 internal class InvalidNodeRecordException(message: String?) : RuntimeException(message)
diff --git a/dns-discovery/build.gradle b/dns-discovery/build.gradle
index 061a2ff..ce375b8 100644
--- a/dns-discovery/build.gradle
+++ b/dns-discovery/build.gradle
@@ -34,4 +34,5 @@ dependencies {
   testCompile 'org.junit.jupiter:junit-jupiter-params'
 
   testRuntime 'org.junit.jupiter:junit-jupiter-engine'
+  testRuntime 'ch.qos.logback:logback-classic'
 }
diff --git a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSEntry.kt b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSEntry.kt
index 51a87fd..6a4a78a 100644
--- a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSEntry.kt
+++ b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSEntry.kt
@@ -21,35 +21,56 @@ 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.apache.tuweni.rlp.InvalidRLPEncodingException
 
 /**
  * Intermediate format to write DNS entries
  */
-internal interface DNSEntry {
+public interface DNSEntry {
 
   companion object {
 
+    /**
+     * Read a DNS entry from a String.
+     * @param serialized the serialized form of a DNS entry
+     * @return DNS entry if found
+     * @throws InvalidEntryException if the record cannot be read
+     */
     fun readDNSEntry(serialized: String): DNSEntry {
-      val attrs = serialized.split(" ").map {
+      var record = serialized
+      if (record[0] == '"') {
+        record = record.substring(1, record.length - 1)
+      }
+      if (record.startsWith("enrtree-root")) {
+        return ENRTreeRoot(readKV(record))
+      } else if (record.startsWith("enrtree-branch")) {
+        return ENRTree(record.substring("enrtree-branch:".length))
+      } else if (record.startsWith("enr:")) {
+        return ENRNode(readKV(record))
+      } else if (record.startsWith("enrtree-link:")) {
+        return ENRTreeLink(readKV(record))
+      } else {
+        throw InvalidEntryException("$serialized should contain enrtree-branch, enr, enrtree-root or enrtree-link")
+      }
+    }
+
+    private fun readKV(record: String): Map<String, String> {
+      return record.split(" ").map {
         val equalseparator = it.indexOf("=")
         if (equalseparator == -1) {
-          throw InvalidEntryException("Invalid record entry $serialized")
+          val colonseparator = it.indexOf(":")
+          if (colonseparator == -1) {
+            throw InvalidEntryException("$it could not be read")
+          }
+          val key = it.substring(0, colonseparator)
+          val value = it.substring(colonseparator + 1)
+          Pair(key, value)
+        } else {
+          val key = it.substring(0, equalseparator)
+          val value = it.substring(equalseparator + 1)
+          Pair(key, value)
         }
-        val key = it.substring(0, equalseparator)
-        val value = it.substring(equalseparator + 1)
-        Pair(key, value)
       }.toMap()
-      if (attrs.containsKey("enrtree-root")) {
-        return ENRTreeRoot(attrs)
-      } else if (attrs.containsKey("enrtree")) {
-        return ENRTree(attrs)
-      } else if (attrs.containsKey("enr")) {
-        return ENRNode(attrs)
-      } else if (attrs.containsKey("enrtree-link")) {
-        return ENRTreeLink(attrs)
-      } else {
-        throw InvalidEntryException("$serialized should contain enrtree, enr, enrtree-root or enrtree-link")
-      }
     }
   }
 }
@@ -62,7 +83,15 @@ class ENRNode(attrs: Map<String, String>) : DNSEntry {
     if (attrs["enr"] == null) {
       throw InvalidEntryException("Missing attributes on enr entry")
     }
-    nodeRecord = EthereumNodeRecord.fromRLP(Base64URLSafe.decode(attrs["enr"]!!))
+    try {
+      nodeRecord = EthereumNodeRecord.fromRLP(Base64URLSafe.decode(attrs["enr"]!!))
+    } catch (e: InvalidRLPEncodingException) {
+      throw InvalidEntryException(e.message)
+    }
+  }
+
+  override fun toString(): String {
+    return nodeRecord.toString()
   }
 }
 
@@ -72,34 +101,40 @@ class ENRTreeRoot(attrs: Map<String, String>) : DNSEntry {
   val seq: Int
   val sig: SECP256K1.Signature
   val hash: Bytes
+  val encodedHash: String
+  val link: String
   init {
-    if (attrs["enrtree-root"] == null || attrs["seq"] == null || attrs["sig"] == null || attrs["hash"] == null) {
+    if (attrs["enrtree-root"] == null || attrs["seq"] == null || attrs["sig"] == null || attrs["e"] == null ||
+      attrs["l"] == null) {
       throw InvalidEntryException("Missing attributes on root entry")
     }
 
     version = attrs["enrtree-root"]!!
     seq = attrs["seq"]!!.toInt()
-    sig = SECP256K1.Signature.fromBytes(Base64URLSafe.decode(attrs["sig"]!!))
-    hash = Base32.decode(attrs["hash"]!!)
+    val sigBytes = Base64URLSafe.decode(attrs["sig"]!!)
+    sig = SECP256K1.Signature.fromBytes(Bytes.concatenate(sigBytes,
+      Bytes.wrap(ByteArray(Math.max(0, 65 - sigBytes.size())))))
+    encodedHash = attrs["e"]!!
+    hash = Base32.decode(encodedHash)
+    link = attrs["l"]!!
   }
 
   override fun toString(): String {
     val encodedHash = Base32.encode(hash)
-    return "enrtree-root=$version hash=${encodedHash.subSequence(0, encodedHash.indexOf("="))} " +
-      "seq=$seq sig=${Base64URLSafe.encode(sig.bytes())}"
+    return "enrtree-root:$version e=${encodedHash.subSequence(0, encodedHash.indexOf("="))} " +
+      "l=$link seq=$seq sig=${Base64URLSafe.encode(sig.bytes())}"
   }
 }
 
-class ENRTree(attrs: Map<String, String>) : DNSEntry {
+class ENRTree(entriesAsString: String) : DNSEntry {
 
   val entries: List<String>
   init {
-    val attr = attrs["enrtree"] ?: throw InvalidEntryException("Missing attributes on enrtree entry")
-    entries = attr.split(",")
+    entries = entriesAsString.split(",", "\"").filter { it.length > 4 }
   }
 
   override fun toString(): String {
-    return "enrtree=${entries.joinToString(",")}"
+    return "enrtree-branch:${entries.joinToString(",")}"
   }
 }
 
diff --git a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSResolver.kt b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSResolver.kt
new file mode 100644
index 0000000..4f6ebdd
--- /dev/null
+++ b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSResolver.kt
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.discovery
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.slf4j.LoggerFactory
+import org.xbill.DNS.DClass
+import org.xbill.DNS.Message
+import org.xbill.DNS.Name
+import org.xbill.DNS.Record
+import org.xbill.DNS.Section
+import org.xbill.DNS.SimpleResolver
+import org.xbill.DNS.Type
+import org.xbill.DNS.WireParseException
+
+/**
+ * Resolves a set of ENR nodes from a host name.
+ *
+ * @param dnsServer the DNS server to use for DNS query. If null, the default DNS server will be used.
+ * @param signingKey the public key associated with the domain, to check that the root DNS record is valid.
+ *
+ */
+class DNSResolver(private val dnsServer: String? = null, private val signingKey: SECP256K1.PublicKey? = null) {
+
+  companion object {
+    val logger = LoggerFactory.getLogger(DNSResolver::class.java)
+  }
+
+  /**
+   * Resolves one DNS record associated with the given domain name.
+   *
+   * @param domainName the domain name to query
+   * @return the DNS entry read from the domain
+   */
+  public fun resolveRecord(domainName: String): DNSEntry? {
+    return resolveRecordRaw(domainName)?.let { DNSEntry.readDNSEntry(it) }
+  }
+
+  private fun checkSignature(entry: ENRTreeRoot, sig: SECP256K1.Signature): Boolean {
+    if (signingKey == null) {
+      return true
+    }
+    TODO("not implemented, $entry $sig")
+    // val recovered = SECP256K1.PublicKey.recoverFromSignature(entry.signedContent(), sig)
+    // return signingKey.equals(recovered)
+  }
+
+  /**
+   * Convenience method to read all ENRs, from a top-level record.
+   *
+   * @param domainName the DNS domain name to start with
+   * @return all ENRs collected
+   */
+  public fun collectAll(domainName: String): List<EthereumNodeRecord> {
+    val nodes = mutableListOf<EthereumNodeRecord>()
+    val visitor = object : DNSVisitor {
+      override fun visit(enr: EthereumNodeRecord): Boolean {
+        nodes.add(enr)
+        return true
+      }
+    }
+    visitTree(domainName, visitor)
+    return nodes
+  }
+
+  /**
+   * Reads a complete tree of record, starting with the top-level record.
+   * @param domainName the DNS domain name to start with
+   * @param visitor the visitor that will look at each record
+   */
+  public fun visitTree(domainName: String, visitor: DNSVisitor) {
+    val entry = resolveRecord(domainName)
+    if (entry !is ENRTreeRoot) {
+      logger.debug("Root domain name $domainName is not an ENR tree root")
+      return
+    }
+    val linkedStr = resolveRecordRaw("${entry.encodedHash}.$domainName")
+    if (linkedStr == null) {
+      logger.debug("No linked record under ${entry.encodedHash}.$domainName")
+      return
+    }
+    if (!checkSignature(entry, entry.sig)) {
+      logger.debug("ENR tree root $domainName failed signature check")
+      return
+    }
+
+    val linkedEntry = DNSEntry.readDNSEntry(linkedStr)
+    // TODO check hash matches
+    if (linkedEntry is ENRNode) {
+      visitor.visit(linkedEntry.nodeRecord)
+      return
+    } else if (linkedEntry is ENRTree) {
+      for (e in linkedEntry.entries) {
+        if (!internalVisit(e, domainName, visitor)) {
+          break
+        }
+      }
+    } else {
+      logger.debug("No linked record")
+    }
+  }
+
+  /**
+   * Resolves the first TXT record associated with a domain name,
+   * and returns it, or null if no such record exists or the record cannot be read.
+   *
+   * @param domainName the name of the DNS domain to query
+   * @return the first TXT entry of the DNS record
+   */
+  fun resolveRecordRaw(domainName: String): String? {
+    try {
+      val resolver = SimpleResolver(dnsServer)
+      // required as TXT records are quite big, and dnsjava maxes out UDP payload.
+      resolver.setTCP(true)
+      val type = Type.TXT
+      val name = Name.fromString(domainName, Name.root)
+      val rec = Record.newRecord(name, type, DClass.IN)
+      val query = Message.newQuery(rec)
+      val response = resolver.send(query)
+      val records = response.getSectionArray(Section.ANSWER)
+
+      if (records.isNotEmpty()) {
+        return records[0].rdataToString()
+      } else {
+        logger.debug("No TXT record for $domainName")
+        return null
+      }
+    } catch (e: WireParseException) {
+      logger.error("Error reading TXT record", e)
+      return null
+    }
+  }
+
+  private fun internalVisit(entryName: String, domainName: String, visitor: DNSVisitor): Boolean {
+    try {
+      val entry = resolveRecord("$entryName.$domainName")
+      if (entry == null) {
+        return true
+      }
+
+    if (entry is ENRNode) {
+      return visitor.visit(entry.nodeRecord)
+    } else if (entry is ENRTree) {
+      for (e in entry.entries) {
+        val keepGoing = internalVisit(e, domainName, visitor)
+        if (!keepGoing) {
+          return false
+        }
+      }
+    } else if (entry is ENRTreeLink) {
+      visitTree(entry.domainName, visitor)
+    } else {
+      logger.debug("Unsupported type of node $entry")
+    }
+    return true
+    } catch (e: InvalidEntryException) {
+      logger.warn(e.message, e)
+      return true
+    } catch (e: IllegalArgumentException) {
+      logger.warn(e.message, e)
+      return true
+    }
+  }
+}
diff --git a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSVisitor.kt b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSVisitor.kt
new file mode 100644
index 0000000..b398807
--- /dev/null
+++ b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSVisitor.kt
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.discovery
+
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+
+/**
+ * Reads ENR (Ethereum Node Records) entries passed in from DNS.
+ *
+ * The visitor may decide to stop the visit by returning false.
+ */
+interface DNSVisitor {
+
+  /**
+   * Visit a new ENR record.
+   * @param enr the ENR record read from DNS
+   * @return true to continue visiting, false otherwise
+   */
+  fun visit(enr: EthereumNodeRecord): Boolean
+}
diff --git a/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSEntryTest.kt b/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSEntryTest.kt
index ce74144..dc13597 100644
--- a/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSEntryTest.kt
+++ b/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSEntryTest.kt
@@ -31,7 +31,7 @@ class DNSEntryTest {
     val exception: InvalidEntryException = assertThrows {
       DNSEntry.readDNSEntry("garbage")
     }
-    assertEquals("Invalid record entry garbage", exception.message)
+    assertEquals("garbage should contain enrtree-branch, enr, enrtree-root or enrtree-link", exception.message)
   }
 
   @Test
@@ -39,7 +39,7 @@ class DNSEntryTest {
     val exception: InvalidEntryException = assertThrows {
       DNSEntry.readDNSEntry("garbage=abc def")
     }
-    assertEquals("Invalid record entry garbage=abc def", exception.message)
+    assertNotNull(exception)
   }
 
   @Test
@@ -47,7 +47,8 @@ class DNSEntryTest {
     val exception: InvalidEntryException = assertThrows {
       DNSEntry.readDNSEntry("garbage=abc def=gfh")
     }
-    assertEquals("garbage=abc def=gfh should contain enrtree, enr, enrtree-root or enrtree-link", exception.message)
+    assertEquals("garbage=abc def=gfh should contain enrtree-branch, enr, enrtree-root or enrtree-link",
+      exception.message)
   }
 
   @Test
@@ -61,16 +62,16 @@ class DNSEntryTest {
   @Test
   fun missingSeqEntry() {
     val exception: InvalidEntryException = assertThrows {
-      DNSEntry.readDNSEntry("enrtree-root=v1 hash=TO4Q75OQ2N7DX4EOOR7X66A6OM " +
-        "sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=")
+      DNSEntry.readDNSEntry("enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM" +
+        " sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=")
     }
     assertEquals("Missing attributes on root entry", exception.message)
   }
 
   @Test
   fun testValidENRTreeRoot() {
-    val entry = DNSEntry.readDNSEntry("enrtree-root=v1 hash=TO4Q75OQ2N7DX4EOOR7X66A6OM " +
-      "seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=")
+    val entry = DNSEntry.readDNSEntry("enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM" +
+      " seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=")
       as ENRTreeRoot
     assertEquals("v1", entry.version)
     assertEquals(3, entry.seq)
@@ -79,15 +80,15 @@ class DNSEntryTest {
   @Test
   fun testValidENRTreeLink() {
     val entry = DNSEntry.readDNSEntry(
-      "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org")
+      "enrtree-link:morenodes.example.org")
       as ENRTreeLink
-    assertEquals("AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org", entry.domainName)
+    assertEquals("morenodes.example.org", entry.domainName)
   }
 
   @Test
   fun testValidENRNode() {
-    val entry = DNSEntry.readDNSEntry("enr=-H24QI0fqW39CMBZjJvV-EJZKyBYIoqvh69kfkF4X8DsJuXOZC6emn53SrrZD8P4v9Wp7Nxg" +
-      "DYwtEUs3zQkxesaGc6UBgmlkgnY0gmlwhMsAcQGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOA==")
+    val entry = DNSEntry.readDNSEntry("enr:-H24QI0fqW39CMBZjJvV-EJZKyBYIoqvh69kfkF4X8DsJuXOZC6emn53SrrZD8P4v9Wp7Nxg" +
+      "DYwtEUs3zQkxesaGc6UBgmlkgnY0gmlwhMsAcQGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOA")
     val enr = entry as ENRNode
     val nodeRecord = enr.nodeRecord
     assertNotNull(nodeRecord)
@@ -96,7 +97,7 @@ class DNSEntryTest {
 
   @Test
   fun testValidENRTreeNode() {
-    val entry = DNSEntry.readDNSEntry("enrtree=F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM," +
+    val entry = DNSEntry.readDNSEntry("enrtree-branch:F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM," +
       "JGUFMSAGI7KZYB3P7IZW4S5Y3A")
     val enr = entry as ENRTree
     val entries = enr.entries
@@ -106,24 +107,26 @@ class DNSEntryTest {
 
   @Test
   fun testRootToString() {
-    val root = ENRTreeRoot(mapOf(Pair("enrtree-root", "v1"), Pair("hash", "TO4Q75OQ2N7DX4EOOR7X66A6OM"),
+    val root = ENRTreeRoot(mapOf(Pair("enrtree-root", "v1"), Pair("l", "TO4Q75OQ2N7DX4EOOR7X66A6OM"),
+      Pair("e", "TO4Q75OQ2N7DX4EOOR7X66A6OM"),
       Pair("seq", "3"),
       Pair("sig", "N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=")))
-    assertEquals("enrtree-root=v1 hash=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry" +
+    assertEquals("enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM " +
+      "l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry" +
       "2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtwE=", root.toString())
   }
 
   @Test
   fun testEntryToString() {
-    val entry = DNSEntry.readDNSEntry("enrtree=F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM," +
+    val entry = DNSEntry.readDNSEntry("enrtree-branch:F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM," +
       "JGUFMSAGI7KZYB3P7IZW4S5Y3A")
-    assertEquals("enrtree=F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM,JGUFMSAGI7KZYB3P7IZW4S5Y3A",
+    assertEquals("enrtree-branch:F4YWVKW4N6B2DDZWFS4XCUQBHY,JTNOVTCP6XZUMXDRANXA6SWXTM,JGUFMSAGI7KZYB3P7IZW4S5Y3A",
       entry.toString())
   }
 
   @Test
   fun testEntryLinkToString() {
-    val entry = DNSEntry.readDNSEntry("enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7B" +
+    val entry = DNSEntry.readDNSEntry("enrtree-link:AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7B" +
       "VDQ5FDPRT2@morenodes.example.org")
     assertEquals("enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
       entry.toString())
diff --git a/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSResolverTest.kt b/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSResolverTest.kt
new file mode 100644
index 0000000..a5b4bf8
--- /dev/null
+++ b/dns-discovery/src/test/kotlin/org/apache/tuweni/discovery/DNSResolverTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tuweni.discovery
+
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(BouncyCastleExtension::class)
+class DNSResolverTest {
+
+  @Test
+  fun testQueryTXT() {
+    val resolver = DNSResolver()
+    val rec = resolver.resolveRecordRaw("all.goerli.ethdisco.net")
+    assertNotNull(rec)
+  }
+
+  @Test
+  fun resolveAllGoerliNodes() {
+    val resolver = DNSResolver()
+    val nodes = mutableListOf<EthereumNodeRecord>()
+    val visitor = object : DNSVisitor {
+      override fun visit(enr: EthereumNodeRecord): Boolean {
+        nodes.add(enr)
+        return true
+      }
+    }
+
+    resolver.visitTree("all.goerli.ethdisco.net", visitor)
+    assertTrue(nodes.size > 0)
+  }
+
+  @Test
+  fun resolveAllMainnetNodes() {
+    val resolver = DNSResolver()
+    val nodes = mutableListOf<EthereumNodeRecord>()
+    val visitor = object : DNSVisitor {
+      override fun visit(enr: EthereumNodeRecord): Boolean {
+        nodes.add(enr)
+        return true
+      }
+    }
+
+    resolver.visitTree("all.mainnet.ethdisco.net", visitor)
+    assertTrue(nodes.size > 0)
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org