Add DNSDaemon to listen to DNS entries
diff --git a/dns-discovery/src/integrationTest/java/org/apache/tuweni/discovery/DiscoveryAPITest.java b/dns-discovery/src/integrationTest/java/org/apache/tuweni/discoveryint/DiscoveryAPITest.java
similarity index 91%
rename from dns-discovery/src/integrationTest/java/org/apache/tuweni/discovery/DiscoveryAPITest.java
rename to dns-discovery/src/integrationTest/java/org/apache/tuweni/discoveryint/DiscoveryAPITest.java
index 80ea34d..6de5a8e 100644
--- a/dns-discovery/src/integrationTest/java/org/apache/tuweni/discovery/DiscoveryAPITest.java
+++ b/dns-discovery/src/integrationTest/java/org/apache/tuweni/discoveryint/DiscoveryAPITest.java
@@ -10,11 +10,13 @@
  * 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;
+package org.apache.tuweni.discoveryint;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.tuweni.devp2p.EthereumNodeRecord;
+import org.apache.tuweni.discovery.DNSResolver;
+import org.apache.tuweni.discovery.DNSVisitor;
 import org.apache.tuweni.junit.BouncyCastleExtension;
 
 import java.util.ArrayList;
diff --git a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSDaemon.kt b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSDaemon.kt
new file mode 100644
index 0000000..17b07f3
--- /dev/null
+++ b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSDaemon.kt
@@ -0,0 +1,75 @@
+/*
+ * 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 java.util.Timer
+import java.util.TimerTask
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Resolves DNS records over time, refreshing records.
+ *
+ * @param dnsServer the DNS server to use for DNS query. If null, the default DNS server will be used.
+ * @param seq the sequence number of the root record. If the root record seq is higher, proceed with visit.
+ * @param enrLink the ENR link to start with, of the form enrtree://PUBKEY@domain
+ * @param period the period at which to poll DNS records
+ */
+public class DNSDaemon @JvmOverloads constructor(
+  private val dnsServer: String? = null,
+  private val seq: Long = 0,
+  private val enrLink: String,
+  private val period: Long = 60000L
+) {
+
+  /**
+   * Listeners notified when records are read and whenever they are updated.
+   */
+  val listeners = HashSet<(List<EthereumNodeRecord>) -> Unit>()
+
+  private val timer: Timer = Timer(false)
+  private val records = AtomicReference<EthereumNodeRecord>()
+
+  init {
+    timer.scheduleAtFixedRate(DNSTimerTask(dnsServer, seq, enrLink, this::updateRecords), 0, period)
+  }
+
+  private fun updateRecords(records: List<EthereumNodeRecord>) {
+    listeners.forEach { it(records) }
+  }
+
+  /**
+   * Close the daemon.
+   */
+  public fun close() {
+    timer.cancel()
+  }
+}
+
+class DNSTimerTask(
+  private val dnsServer: String? = null,
+  private var seq: Long,
+  private val enrLink: String,
+  private val records: (List<EthereumNodeRecord>) -> Unit
+) : TimerTask() {
+
+  override fun run() {
+    val resolver = DNSResolver(dnsServer, seq)
+    records(resolver.collectAll(enrLink))
+    seq = resolver.seq
+  }
+}
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 5e3a6bf..085c95c 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
@@ -99,7 +99,7 @@
 class ENRTreeRoot(attrs: Map<String, String>) : DNSEntry {
 
   val version: String
-  val seq: Int
+  val seq: Long
   val sig: SECP256K1.Signature
   val enrRoot: String
   val linkRoot: String
@@ -110,7 +110,7 @@
     }
 
     version = attrs["enrtree-root"]!!
-    seq = attrs["seq"]!!.toInt()
+    seq = attrs["seq"]!!.toLong()
     val sigBytes = Base64URLSafe.decode(attrs["sig"]!!)
     sig = SECP256K1.Signature.fromBytes(Bytes.concatenate(sigBytes,
       Bytes.wrap(ByteArray(Math.max(0, 65 - sigBytes.size())))))
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
index db0d38d..503715d 100644
--- a/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSResolver.kt
+++ b/dns-discovery/src/main/kotlin/org/apache/tuweni/discovery/DNSResolver.kt
@@ -49,10 +49,13 @@
  * 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.
- *
+ * @param seq the sequence number of the root record. If the root record seq is higher, proceed with visit.
  */
-class DNSResolver(private val dnsServer: String? = null, private val signingKey: SECP256K1.PublicKey? = null) {
+
+public class DNSResolver @JvmOverloads constructor(
+  private val dnsServer: String? = null,
+  var seq: Long = 0
+) {
 
   companion object {
     val logger = LoggerFactory.getLogger(DNSResolver::class.java)
@@ -116,6 +119,12 @@
       logger.debug("ENR tree root ${link.domainName} failed signature check")
       return
     }
+    if (entry.seq <= seq) {
+      logger.debug("ENR tree root seq $entry.seq is not higher than $seq, aborting")
+      return
+    }
+    seq = entry.seq
+
     internalVisit(entry.enrRoot, link.domainName, visitor)
     internalVisit(entry.linkRoot, link.domainName, visitor)
   }