diff --git a/p2p/dnsdisc/client.go b/p2p/dnsdisc/client.go
new file mode 100644
index 0000000000..d4ed08ecb7
--- /dev/null
+++ b/p2p/dnsdisc/client.go
@@ -0,0 +1,260 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "math/rand"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ lru "github.com/hashicorp/golang-lru"
+)
+
+// Client discovers nodes by querying DNS servers.
+type Client struct {
+ cfg Config
+ clock mclock.Clock
+ linkCache linkCache
+ trees map[string]*clientTree
+
+ entries *lru.Cache
+}
+
+// Config holds configuration options for the client.
+type Config struct {
+ Timeout time.Duration // timeout used for DNS lookups (default 5s)
+ RecheckInterval time.Duration // time between tree root update checks (default 30min)
+ CacheLimit int // maximum number of cached records (default 1000)
+ ValidSchemes enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes)
+ Resolver Resolver // the DNS resolver to use (defaults to system DNS)
+ Logger log.Logger // destination of client log messages (defaults to root logger)
+}
+
+// Resolver is a DNS resolver that can query TXT records.
+type Resolver interface {
+ LookupTXT(ctx context.Context, domain string) ([]string, error)
+}
+
+func (cfg Config) withDefaults() Config {
+ const (
+ defaultTimeout = 5 * time.Second
+ defaultRecheck = 30 * time.Minute
+ defaultCache = 1000
+ )
+ if cfg.Timeout == 0 {
+ cfg.Timeout = defaultTimeout
+ }
+ if cfg.RecheckInterval == 0 {
+ cfg.RecheckInterval = defaultRecheck
+ }
+ if cfg.CacheLimit == 0 {
+ cfg.CacheLimit = defaultCache
+ }
+ if cfg.ValidSchemes == nil {
+ cfg.ValidSchemes = enode.ValidSchemes
+ }
+ if cfg.Resolver == nil {
+ cfg.Resolver = new(net.Resolver)
+ }
+ if cfg.Logger == nil {
+ cfg.Logger = log.Root()
+ }
+ return cfg
+}
+
+// NewClient creates a client.
+func NewClient(cfg Config, urls ...string) (*Client, error) {
+ c := &Client{
+ cfg: cfg.withDefaults(),
+ clock: mclock.System{},
+ trees: make(map[string]*clientTree),
+ }
+ var err error
+ if c.entries, err = lru.New(c.cfg.CacheLimit); err != nil {
+ return nil, err
+ }
+ for _, url := range urls {
+ if err := c.AddTree(url); err != nil {
+ return nil, err
+ }
+ }
+ return c, nil
+}
+
+// SyncTree downloads the entire node tree at the given URL. This doesn't add the tree for
+// later use, but any previously-synced entries are reused.
+func (c *Client) SyncTree(url string) (*Tree, error) {
+ le, err := parseURL(url)
+ if err != nil {
+ return nil, fmt.Errorf("invalid enrtree URL: %v", err)
+ }
+ ct := newClientTree(c, le)
+ t := &Tree{entries: make(map[string]entry)}
+ if err := ct.syncAll(t.entries); err != nil {
+ return nil, err
+ }
+ t.root = ct.root
+ return t, nil
+}
+
+// AddTree adds a enrtree:// URL to crawl.
+func (c *Client) AddTree(url string) error {
+ le, err := parseURL(url)
+ if err != nil {
+ return fmt.Errorf("invalid enrtree URL: %v", err)
+ }
+ ct, err := c.ensureTree(le)
+ if err != nil {
+ return err
+ }
+ c.linkCache.add(ct)
+ return nil
+}
+
+func (c *Client) ensureTree(le *linkEntry) (*clientTree, error) {
+ if tree, ok := c.trees[le.domain]; ok {
+ if !tree.matchPubkey(le.pubkey) {
+ return nil, fmt.Errorf("conflicting public keys for domain %q", le.domain)
+ }
+ return tree, nil
+ }
+ ct := newClientTree(c, le)
+ c.trees[le.domain] = ct
+ return ct, nil
+}
+
+// RandomNode retrieves the next random node.
+func (c *Client) RandomNode(ctx context.Context) *enode.Node {
+ for {
+ ct := c.randomTree()
+ if ct == nil {
+ return nil
+ }
+ n, err := ct.syncRandom(ctx)
+ if err != nil {
+ if err == ctx.Err() {
+ return nil // context canceled.
+ }
+ c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
+ continue
+ }
+ if n != nil {
+ return n
+ }
+ }
+}
+
+// randomTree returns a random tree.
+func (c *Client) randomTree() *clientTree {
+ if !c.linkCache.valid() {
+ c.gcTrees()
+ }
+ limit := rand.Intn(len(c.trees))
+ for _, ct := range c.trees {
+ if limit == 0 {
+ return ct
+ }
+ limit--
+ }
+ return nil
+}
+
+// gcTrees rebuilds the 'trees' map.
+func (c *Client) gcTrees() {
+ trees := make(map[string]*clientTree)
+ for t := range c.linkCache.all() {
+ trees[t.loc.domain] = t
+ }
+ c.trees = trees
+}
+
+// resolveRoot retrieves a root entry via DNS.
+func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
+ txts, err := c.cfg.Resolver.LookupTXT(ctx, loc.domain)
+ c.cfg.Logger.Trace("Updating DNS discovery root", "tree", loc.domain, "err", err)
+ if err != nil {
+ return rootEntry{}, err
+ }
+ for _, txt := range txts {
+ if strings.HasPrefix(txt, rootPrefix) {
+ return parseAndVerifyRoot(txt, loc)
+ }
+ }
+ return rootEntry{}, nameError{loc.domain, errNoRoot}
+}
+
+func parseAndVerifyRoot(txt string, loc *linkEntry) (rootEntry, error) {
+ e, err := parseRoot(txt)
+ if err != nil {
+ return e, err
+ }
+ if !e.verifySignature(loc.pubkey) {
+ return e, entryError{typ: "root", err: errInvalidSig}
+ }
+ return e, nil
+}
+
+// resolveEntry retrieves an entry from the cache or fetches it from the network
+// if it isn't cached.
+func (c *Client) resolveEntry(ctx context.Context, domain, hash string) (entry, error) {
+ cacheKey := truncateHash(hash)
+ if e, ok := c.entries.Get(cacheKey); ok {
+ return e.(entry), nil
+ }
+ e, err := c.doResolveEntry(ctx, domain, hash)
+ if err != nil {
+ return nil, err
+ }
+ c.entries.Add(cacheKey, e)
+ return e, nil
+}
+
+// doResolveEntry fetches an entry via DNS.
+func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry, error) {
+ wantHash, err := b32format.DecodeString(hash)
+ if err != nil {
+ return nil, fmt.Errorf("invalid base32 hash")
+ }
+ name := hash + "." + domain
+ txts, err := c.cfg.Resolver.LookupTXT(ctx, hash+"."+domain)
+ c.cfg.Logger.Trace("DNS discovery lookup", "name", name, "err", err)
+ if err != nil {
+ return nil, err
+ }
+ for _, txt := range txts {
+ e, err := parseEntry(txt, c.cfg.ValidSchemes)
+ if err == errUnknownEntry {
+ continue
+ }
+ if !bytes.HasPrefix(crypto.Keccak256([]byte(txt)), wantHash) {
+ err = nameError{name, errHashMismatch}
+ } else if err != nil {
+ err = nameError{name, err}
+ }
+ return e, err
+ }
+ return nil, nameError{name, errNoEntry}
+}
diff --git a/p2p/dnsdisc/client_test.go b/p2p/dnsdisc/client_test.go
new file mode 100644
index 0000000000..7e3a0f4826
--- /dev/null
+++ b/p2p/dnsdisc/client_test.go
@@ -0,0 +1,306 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "math/rand"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/testlog"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+)
+
+const (
+ signingKeySeed = 0x111111
+ nodesSeed1 = 0x2945237
+ nodesSeed2 = 0x4567299
+)
+
+func TestClientSyncTree(t *testing.T) {
+ r := mapResolver{
+ "3CA2MBMUQ55ZCT74YEEQLANJDI.n": "enr=-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI=",
+ "53HBTPGGZ4I76UEPCNQGZWIPTQ.n": "enr=-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA=",
+ "BG7SVUBUAJ3UAWD2ATEBLMRNEE.n": "enrtree=53HBTPGGZ4I76UEPCNQGZWIPTQ,3CA2MBMUQ55ZCT74YEEQLANJDI,HNHR6UTVZF5TJKK3FV27ZI76P4",
+ "HNHR6UTVZF5TJKK3FV27ZI76P4.n": "enr=-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o=",
+ "JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
+ "n": "enrtree-root=v1 e=BG7SVUBUAJ3UAWD2ATEBLMRNEE l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=1 sig=gacuU0nTy9duIdu1IFDyF5Lv9CFHqHiNcj91n0frw70tZo3tZZsCVkE3j1ILYyVOHRLWGBmawo_SEkThZ9PgcQE=",
+ }
+ var (
+ wantNodes = testNodes(0x29452, 3)
+ wantLinks = []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"}
+ wantSeq = uint(1)
+ )
+
+ c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+ stree, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
+ if err != nil {
+ t.Fatal("sync error:", err)
+ }
+ if !reflect.DeepEqual(sortByID(stree.Nodes()), sortByID(wantNodes)) {
+ t.Errorf("wrong nodes in synced tree:\nhave %v\nwant %v", spew.Sdump(stree.Nodes()), spew.Sdump(wantNodes))
+ }
+ if !reflect.DeepEqual(stree.Links(), wantLinks) {
+ t.Errorf("wrong links in synced tree: %v", stree.Links())
+ }
+ if stree.Seq() != wantSeq {
+ t.Errorf("synced tree has wrong seq: %d", stree.Seq())
+ }
+ if len(c.trees) > 0 {
+ t.Errorf("tree from SyncTree added to client")
+ }
+}
+
+// In this test, syncing the tree fails because it contains an invalid ENR entry.
+func TestClientSyncTreeBadNode(t *testing.T) {
+ r := mapResolver{
+ "n": "enrtree-root=v1 e=ZFJZDQKSOMJRYYQSZKJZC54HCF l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=WEy8JTZ2dHmXM2qeBZ7D2ECK7SGbnurl1ge_S_5GQBAqnADk0gLTcg8Lm5QNqLHZjJKGAb443p996idlMcBqEQA=",
+ "JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
+ "ZFJZDQKSOMJRYYQSZKJZC54HCF.n": "enr=gggggggggggggg=",
+ }
+
+ c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+ _, err := c.SyncTree("enrtree://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@n")
+ wantErr := nameError{name: "ZFJZDQKSOMJRYYQSZKJZC54HCF.n", err: entryError{typ: "enr", err: errInvalidENR}}
+ if err != wantErr {
+ t.Fatalf("expected sync error %q, got %q", wantErr, err)
+ }
+}
+
+// This test checks that RandomNode hits all entries.
+func TestClientRandomNode(t *testing.T) {
+ nodes := testNodes(nodesSeed1, 30)
+ tree, url := makeTestTree("n", nodes, nil)
+ r := mapResolver(tree.ToTXT("n"))
+ c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+ if err := c.AddTree(url); err != nil {
+ t.Fatal(err)
+ }
+
+ checkRandomNode(t, c, nodes)
+}
+
+// This test checks that RandomNode traverses linked trees as well as explicitly added trees.
+func TestClientRandomNodeLinks(t *testing.T) {
+ nodes := testNodes(nodesSeed1, 40)
+ tree1, url1 := makeTestTree("t1", nodes[:10], nil)
+ tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1})
+ cfg := Config{
+ Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
+ Logger: testlog.Logger(t, log.LvlTrace),
+ }
+ c, _ := NewClient(cfg)
+ if err := c.AddTree(url2); err != nil {
+ t.Fatal(err)
+ }
+
+ checkRandomNode(t, c, nodes)
+}
+
+// This test verifies that RandomNode re-checks the root of the tree to catch
+// updates to nodes.
+func TestClientRandomNodeUpdates(t *testing.T) {
+ var (
+ clock = new(mclock.Simulated)
+ nodes = testNodes(nodesSeed1, 30)
+ resolver = newMapResolver()
+ cfg = Config{
+ Resolver: resolver,
+ Logger: testlog.Logger(t, log.LvlTrace),
+ RecheckInterval: 20 * time.Minute,
+ }
+ c, _ = NewClient(cfg)
+ )
+ c.clock = clock
+ tree1, url := makeTestTree("n", nodes[:25], nil)
+
+ // Sync the original tree.
+ resolver.add(tree1.ToTXT("n"))
+ c.AddTree(url)
+ checkRandomNode(t, c, nodes[:25])
+
+ // Update some nodes and ensure RandomNode returns the new nodes as well.
+ keys := testKeys(nodesSeed1, len(nodes))
+ for i, n := range nodes[:len(nodes)/2] {
+ r := n.Record()
+ r.Set(enr.IP{127, 0, 0, 1})
+ r.SetSeq(55)
+ enode.SignV4(r, keys[i])
+ n2, _ := enode.New(enode.ValidSchemes, r)
+ nodes[i] = n2
+ }
+ tree2, _ := makeTestTree("n", nodes, nil)
+ clock.Run(cfg.RecheckInterval + 1*time.Second)
+ resolver.clear()
+ resolver.add(tree2.ToTXT("n"))
+ checkRandomNode(t, c, nodes)
+}
+
+// This test verifies that RandomNode re-checks the root of the tree to catch
+// updates to links.
+func TestClientRandomNodeLinkUpdates(t *testing.T) {
+ var (
+ clock = new(mclock.Simulated)
+ nodes = testNodes(nodesSeed1, 30)
+ resolver = newMapResolver()
+ cfg = Config{
+ Resolver: resolver,
+ Logger: testlog.Logger(t, log.LvlTrace),
+ RecheckInterval: 20 * time.Minute,
+ }
+ c, _ = NewClient(cfg)
+ )
+ c.clock = clock
+ tree3, url3 := makeTestTree("t3", nodes[20:30], nil)
+ tree2, url2 := makeTestTree("t2", nodes[10:20], nil)
+ tree1, url1 := makeTestTree("t1", nodes[0:10], []string{url2})
+ resolver.add(tree1.ToTXT("t1"))
+ resolver.add(tree2.ToTXT("t2"))
+ resolver.add(tree3.ToTXT("t3"))
+
+ // Sync tree1 using RandomNode.
+ c.AddTree(url1)
+ checkRandomNode(t, c, nodes[:20])
+
+ // Add link to tree3, remove link to tree2.
+ tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
+ resolver.add(tree1.ToTXT("t1"))
+ clock.Run(cfg.RecheckInterval + 1*time.Second)
+ t.Log("tree1 updated")
+
+ var wantNodes []*enode.Node
+ wantNodes = append(wantNodes, tree1.Nodes()...)
+ wantNodes = append(wantNodes, tree3.Nodes()...)
+ checkRandomNode(t, c, wantNodes)
+
+ // Check that linked trees are GCed when they're no longer referenced.
+ if len(c.trees) != 2 {
+ t.Errorf("client knows %d trees, want 2", len(c.trees))
+ }
+}
+
+func checkRandomNode(t *testing.T, c *Client, wantNodes []*enode.Node) {
+ t.Helper()
+
+ var (
+ want = make(map[enode.ID]*enode.Node)
+ maxCalls = len(wantNodes) * 2
+ calls = 0
+ ctx = context.Background()
+ )
+ for _, n := range wantNodes {
+ want[n.ID()] = n
+ }
+ for ; len(want) > 0 && calls < maxCalls; calls++ {
+ n := c.RandomNode(ctx)
+ if n == nil {
+ t.Fatalf("RandomNode returned nil (call %d)", calls)
+ }
+ delete(want, n.ID())
+ }
+ t.Logf("checkRandomNode called RandomNode %d times to find %d nodes", calls, len(wantNodes))
+ for _, n := range want {
+ t.Errorf("RandomNode didn't discover node %v", n.ID())
+ }
+}
+
+func makeTestTree(domain string, nodes []*enode.Node, links []string) (*Tree, string) {
+ tree, err := MakeTree(1, nodes, links)
+ if err != nil {
+ panic(err)
+ }
+ url, err := tree.Sign(testKey(signingKeySeed), domain)
+ if err != nil {
+ panic(err)
+ }
+ return tree, url
+}
+
+// testKeys creates deterministic private keys for testing.
+func testKeys(seed int64, n int) []*ecdsa.PrivateKey {
+ rand := rand.New(rand.NewSource(seed))
+ keys := make([]*ecdsa.PrivateKey, n)
+ for i := 0; i < n; i++ {
+ key, err := ecdsa.GenerateKey(crypto.S256(), rand)
+ if err != nil {
+ panic("can't generate key: " + err.Error())
+ }
+ keys[i] = key
+ }
+ return keys
+}
+
+func testKey(seed int64) *ecdsa.PrivateKey {
+ return testKeys(seed, 1)[0]
+}
+
+func testNodes(seed int64, n int) []*enode.Node {
+ keys := testKeys(seed, n)
+ nodes := make([]*enode.Node, n)
+ for i, key := range keys {
+ record := new(enr.Record)
+ record.SetSeq(uint64(i))
+ enode.SignV4(record, key)
+ n, err := enode.New(enode.ValidSchemes, record)
+ if err != nil {
+ panic(err)
+ }
+ nodes[i] = n
+ }
+ return nodes
+}
+
+func testNode(seed int64) *enode.Node {
+ return testNodes(seed, 1)[0]
+}
+
+type mapResolver map[string]string
+
+func newMapResolver(maps ...map[string]string) mapResolver {
+ mr := make(mapResolver)
+ for _, m := range maps {
+ mr.add(m)
+ }
+ return mr
+}
+
+func (mr mapResolver) clear() {
+ for k := range mr {
+ delete(mr, k)
+ }
+}
+
+func (mr mapResolver) add(m map[string]string) {
+ for k, v := range m {
+ mr[k] = v
+ }
+}
+
+func (mr mapResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
+ if record, ok := mr[name]; ok {
+ return []string{record}, nil
+ }
+ return nil, nil
+}
diff --git a/p2p/dnsdisc/doc.go b/p2p/dnsdisc/doc.go
new file mode 100644
index 0000000000..227467d084
--- /dev/null
+++ b/p2p/dnsdisc/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package dnsdisc implements node discovery via DNS (EIP-1459).
+package dnsdisc
diff --git a/p2p/dnsdisc/error.go b/p2p/dnsdisc/error.go
new file mode 100644
index 0000000000..e0998c7350
--- /dev/null
+++ b/p2p/dnsdisc/error.go
@@ -0,0 +1,63 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "errors"
+ "fmt"
+)
+
+// Entry parse errors.
+var (
+ errUnknownEntry = errors.New("unknown entry type")
+ errNoPubkey = errors.New("missing public key")
+ errBadPubkey = errors.New("invalid public key")
+ errInvalidENR = errors.New("invalid node record")
+ errInvalidChild = errors.New("invalid child hash")
+ errInvalidSig = errors.New("invalid base64 signature")
+ errSyntax = errors.New("invalid syntax")
+)
+
+// Resolver/sync errors
+var (
+ errNoRoot = errors.New("no valid root found")
+ errNoEntry = errors.New("no valid tree entry found")
+ errHashMismatch = errors.New("hash mismatch")
+ errENRInLinkTree = errors.New("enr entry in link tree")
+ errLinkInENRTree = errors.New("link entry in ENR tree")
+)
+
+type nameError struct {
+ name string
+ err error
+}
+
+func (err nameError) Error() string {
+ if ee, ok := err.err.(entryError); ok {
+ return fmt.Sprintf("invalid %s entry at %s: %v", ee.typ, err.name, ee.err)
+ }
+ return err.name + ": " + err.err.Error()
+}
+
+type entryError struct {
+ typ string
+ err error
+}
+
+func (err entryError) Error() string {
+ return fmt.Sprintf("invalid %s entry: %v", err.typ, err.err)
+}
diff --git a/p2p/dnsdisc/sync.go b/p2p/dnsdisc/sync.go
new file mode 100644
index 0000000000..bd5c8d023f
--- /dev/null
+++ b/p2p/dnsdisc/sync.go
@@ -0,0 +1,277 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "math/rand"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+// clientTree is a full tree being synced.
+type clientTree struct {
+ c *Client
+ loc *linkEntry
+ root *rootEntry
+ lastRootCheck mclock.AbsTime // last revalidation of root
+ enrs *subtreeSync
+ links *subtreeSync
+ linkCache linkCache
+}
+
+func newClientTree(c *Client, loc *linkEntry) *clientTree {
+ ct := &clientTree{c: c, loc: loc}
+ ct.linkCache.self = ct
+ return ct
+}
+
+func (ct *clientTree) matchPubkey(key *ecdsa.PublicKey) bool {
+ return keysEqual(ct.loc.pubkey, key)
+}
+
+func keysEqual(k1, k2 *ecdsa.PublicKey) bool {
+ return k1.Curve == k2.Curve && k1.X.Cmp(k2.X) == 0 && k1.Y.Cmp(k2.Y) == 0
+}
+
+// syncAll retrieves all entries of the tree.
+func (ct *clientTree) syncAll(dest map[string]entry) error {
+ if err := ct.updateRoot(); err != nil {
+ return err
+ }
+ if err := ct.links.resolveAll(dest); err != nil {
+ return err
+ }
+ if err := ct.enrs.resolveAll(dest); err != nil {
+ return err
+ }
+ return nil
+}
+
+// syncRandom retrieves a single entry of the tree. The Node return value
+// is non-nil if the entry was a node.
+func (ct *clientTree) syncRandom(ctx context.Context) (*enode.Node, error) {
+ if ct.rootUpdateDue() {
+ if err := ct.updateRoot(); err != nil {
+ return nil, err
+ }
+ }
+ // Link tree sync has priority, run it to completion before syncing ENRs.
+ if !ct.links.done() {
+ err := ct.syncNextLink(ctx)
+ return nil, err
+ }
+
+ // Sync next random entry in ENR tree. Once every node has been visited, we simply
+ // start over. This is fine because entries are cached.
+ if ct.enrs.done() {
+ ct.enrs = newSubtreeSync(ct.c, ct.loc, ct.root.eroot, false)
+ }
+ return ct.syncNextRandomENR(ctx)
+}
+
+func (ct *clientTree) syncNextLink(ctx context.Context) error {
+ hash := ct.links.missing[0]
+ e, err := ct.links.resolveNext(ctx, hash)
+ if err != nil {
+ return err
+ }
+ ct.links.missing = ct.links.missing[1:]
+
+ if le, ok := e.(*linkEntry); ok {
+ lt, err := ct.c.ensureTree(le)
+ if err != nil {
+ return err
+ }
+ ct.linkCache.add(lt)
+ }
+ return nil
+}
+
+func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error) {
+ index := rand.Intn(len(ct.enrs.missing))
+ hash := ct.enrs.missing[index]
+ e, err := ct.enrs.resolveNext(ctx, hash)
+ if err != nil {
+ return nil, err
+ }
+ ct.enrs.missing = removeHash(ct.enrs.missing, index)
+ if ee, ok := e.(*enrEntry); ok {
+ return ee.node, nil
+ }
+ return nil, nil
+}
+
+func (ct *clientTree) String() string {
+ return ct.loc.url()
+}
+
+// removeHash removes the element at index from h.
+func removeHash(h []string, index int) []string {
+ if len(h) == 1 {
+ return nil
+ }
+ last := len(h) - 1
+ if index < last {
+ h[index] = h[last]
+ h[last] = ""
+ }
+ return h[:last]
+}
+
+// updateRoot ensures that the given tree has an up-to-date root.
+func (ct *clientTree) updateRoot() error {
+ ct.lastRootCheck = ct.c.clock.Now()
+ ctx, cancel := context.WithTimeout(context.Background(), ct.c.cfg.Timeout)
+ defer cancel()
+ root, err := ct.c.resolveRoot(ctx, ct.loc)
+ if err != nil {
+ return err
+ }
+ ct.root = &root
+
+ // Invalidate subtrees if changed.
+ if ct.links == nil || root.lroot != ct.links.root {
+ ct.links = newSubtreeSync(ct.c, ct.loc, root.lroot, true)
+ ct.linkCache.reset()
+ }
+ if ct.enrs == nil || root.eroot != ct.enrs.root {
+ ct.enrs = newSubtreeSync(ct.c, ct.loc, root.eroot, false)
+ }
+ return nil
+}
+
+// rootUpdateDue returns true when a root update is needed.
+func (ct *clientTree) rootUpdateDue() bool {
+ return ct.root == nil || time.Duration(ct.c.clock.Now()-ct.lastRootCheck) > ct.c.cfg.RecheckInterval
+}
+
+// subtreeSync is the sync of an ENR or link subtree.
+type subtreeSync struct {
+ c *Client
+ loc *linkEntry
+ root string
+ missing []string // missing tree node hashes
+ link bool // true if this sync is for the link tree
+}
+
+func newSubtreeSync(c *Client, loc *linkEntry, root string, link bool) *subtreeSync {
+ return &subtreeSync{c, loc, root, []string{root}, link}
+}
+
+func (ts *subtreeSync) done() bool {
+ return len(ts.missing) == 0
+}
+
+func (ts *subtreeSync) resolveAll(dest map[string]entry) error {
+ for !ts.done() {
+ hash := ts.missing[0]
+ ctx, cancel := context.WithTimeout(context.Background(), ts.c.cfg.Timeout)
+ e, err := ts.resolveNext(ctx, hash)
+ cancel()
+ if err != nil {
+ return err
+ }
+ dest[hash] = e
+ ts.missing = ts.missing[1:]
+ }
+ return nil
+}
+
+func (ts *subtreeSync) resolveNext(ctx context.Context, hash string) (entry, error) {
+ e, err := ts.c.resolveEntry(ctx, ts.loc.domain, hash)
+ if err != nil {
+ return nil, err
+ }
+ switch e := e.(type) {
+ case *enrEntry:
+ if ts.link {
+ return nil, errENRInLinkTree
+ }
+ case *linkEntry:
+ if !ts.link {
+ return nil, errLinkInENRTree
+ }
+ case *subtreeEntry:
+ ts.missing = append(ts.missing, e.children...)
+ }
+ return e, nil
+}
+
+// linkCache tracks the links of a tree.
+type linkCache struct {
+ self *clientTree
+ directM map[*clientTree]struct{} // direct links
+ allM map[*clientTree]struct{} // direct & transitive links
+}
+
+// reset clears the cache.
+func (lc *linkCache) reset() {
+ lc.directM = nil
+ lc.allM = nil
+}
+
+// add adds a direct link to the cache.
+func (lc *linkCache) add(ct *clientTree) {
+ if lc.directM == nil {
+ lc.directM = make(map[*clientTree]struct{})
+ }
+ if _, ok := lc.directM[ct]; !ok {
+ lc.invalidate()
+ }
+ lc.directM[ct] = struct{}{}
+}
+
+// invalidate resets the cache of transitive links.
+func (lc *linkCache) invalidate() {
+ lc.allM = nil
+}
+
+// valid returns true when the cache of transitive links is up-to-date.
+func (lc *linkCache) valid() bool {
+ // Re-check validity of child caches to catch updates.
+ for ct := range lc.allM {
+ if ct != lc.self && !ct.linkCache.valid() {
+ lc.allM = nil
+ break
+ }
+ }
+ return lc.allM != nil
+}
+
+// all returns all trees reachable through the cache.
+func (lc *linkCache) all() map[*clientTree]struct{} {
+ if lc.valid() {
+ return lc.allM
+ }
+ // Remake lc.allM it by taking the union of all() across children.
+ m := make(map[*clientTree]struct{})
+ if lc.self != nil {
+ m[lc.self] = struct{}{}
+ }
+ for ct := range lc.directM {
+ m[ct] = struct{}{}
+ for lt := range ct.linkCache.all() {
+ m[lt] = struct{}{}
+ }
+ }
+ lc.allM = m
+ return m
+}
diff --git a/p2p/dnsdisc/tree.go b/p2p/dnsdisc/tree.go
new file mode 100644
index 0000000000..855d4968c5
--- /dev/null
+++ b/p2p/dnsdisc/tree.go
@@ -0,0 +1,384 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "encoding/base32"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+ "github.com/ethereum/go-ethereum/p2p/enr"
+ "github.com/ethereum/go-ethereum/rlp"
+ "golang.org/x/crypto/sha3"
+)
+
+// Tree is a merkle tree of node records.
+type Tree struct {
+ root *rootEntry
+ entries map[string]entry
+}
+
+// Sign signs the tree with the given private key and sets the sequence number.
+func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error) {
+ root := *t.root
+ sig, err := crypto.Sign(root.sigHash(), key)
+ if err != nil {
+ return "", err
+ }
+ root.sig = sig
+ t.root = &root
+ link := &linkEntry{domain, &key.PublicKey}
+ return link.url(), nil
+}
+
+// SetSignature verifies the given signature and assigns it as the tree's current
+// signature if valid.
+func (t *Tree) SetSignature(pubkey *ecdsa.PublicKey, signature string) error {
+ sig, err := b64format.DecodeString(signature)
+ if err != nil || len(sig) != crypto.SignatureLength {
+ return errInvalidSig
+ }
+ root := *t.root
+ root.sig = sig
+ if !root.verifySignature(pubkey) {
+ return errInvalidSig
+ }
+ t.root = &root
+ return nil
+}
+
+// Seq returns the sequence number of the tree.
+func (t *Tree) Seq() uint {
+ return t.root.seq
+}
+
+// Signature returns the signature of the tree.
+func (t *Tree) Signature() string {
+ return b64format.EncodeToString(t.root.sig)
+}
+
+// ToTXT returns all DNS TXT records required for the tree.
+func (t *Tree) ToTXT(domain string) map[string]string {
+ records := map[string]string{domain: t.root.String()}
+ for _, e := range t.entries {
+ sd := subdomain(e)
+ if domain != "" {
+ sd = sd + "." + domain
+ }
+ records[sd] = e.String()
+ }
+ return records
+}
+
+// Links returns all links contained in the tree.
+func (t *Tree) Links() []string {
+ var links []string
+ for _, e := range t.entries {
+ if le, ok := e.(*linkEntry); ok {
+ links = append(links, le.url())
+ }
+ }
+ return links
+}
+
+// Nodes returns all nodes contained in the tree.
+func (t *Tree) Nodes() []*enode.Node {
+ var nodes []*enode.Node
+ for _, e := range t.entries {
+ if ee, ok := e.(*enrEntry); ok {
+ nodes = append(nodes, ee.node)
+ }
+ }
+ return nodes
+}
+
+const (
+ hashAbbrev = 16
+ maxChildren = 300 / (hashAbbrev * (13 / 8))
+ minHashLength = 12
+ rootPrefix = "enrtree-root=v1"
+)
+
+// MakeTree creates a tree containing the given nodes and links.
+func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
+ // Sort records by ID and ensure all nodes have a valid record.
+ records := make([]*enode.Node, len(nodes))
+ copy(records, nodes)
+ sortByID(records)
+ for _, n := range records {
+ if len(n.Record().Signature()) == 0 {
+ return nil, fmt.Errorf("can't add node %v: unsigned node record", n.ID())
+ }
+ }
+
+ // Create the leaf list.
+ enrEntries := make([]entry, len(records))
+ for i, r := range records {
+ enrEntries[i] = &enrEntry{r}
+ }
+ linkEntries := make([]entry, len(links))
+ for i, l := range links {
+ le, err := parseURL(l)
+ if err != nil {
+ return nil, err
+ }
+ linkEntries[i] = le
+ }
+
+ // Create intermediate nodes.
+ t := &Tree{entries: make(map[string]entry)}
+ eroot := t.build(enrEntries)
+ t.entries[subdomain(eroot)] = eroot
+ lroot := t.build(linkEntries)
+ t.entries[subdomain(lroot)] = lroot
+ t.root = &rootEntry{seq: seq, eroot: subdomain(eroot), lroot: subdomain(lroot)}
+ return t, nil
+}
+
+func (t *Tree) build(entries []entry) entry {
+ if len(entries) == 1 {
+ return entries[0]
+ }
+ if len(entries) <= maxChildren {
+ hashes := make([]string, len(entries))
+ for i, e := range entries {
+ hashes[i] = subdomain(e)
+ t.entries[hashes[i]] = e
+ }
+ return &subtreeEntry{hashes}
+ }
+ var subtrees []entry
+ for len(entries) > 0 {
+ n := maxChildren
+ if len(entries) < n {
+ n = len(entries)
+ }
+ sub := t.build(entries[:n])
+ entries = entries[n:]
+ subtrees = append(subtrees, sub)
+ t.entries[subdomain(sub)] = sub
+ }
+ return t.build(subtrees)
+}
+
+func sortByID(nodes []*enode.Node) []*enode.Node {
+ sort.Slice(nodes, func(i, j int) bool {
+ return bytes.Compare(nodes[i].ID().Bytes(), nodes[j].ID().Bytes()) < 0
+ })
+ return nodes
+}
+
+// Entry Types
+
+type entry interface {
+ fmt.Stringer
+}
+
+type (
+ rootEntry struct {
+ eroot string
+ lroot string
+ seq uint
+ sig []byte
+ }
+ subtreeEntry struct {
+ children []string
+ }
+ enrEntry struct {
+ node *enode.Node
+ }
+ linkEntry struct {
+ domain string
+ pubkey *ecdsa.PublicKey
+ }
+)
+
+// Entry Encoding
+
+var (
+ b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
+ b64format = base64.URLEncoding
+)
+
+func subdomain(e entry) string {
+ h := sha3.NewLegacyKeccak256()
+ io.WriteString(h, e.String())
+ return b32format.EncodeToString(h.Sum(nil)[:16])
+}
+
+func (e *rootEntry) String() string {
+ return fmt.Sprintf(rootPrefix+" e=%s l=%s seq=%d sig=%s", e.eroot, e.lroot, e.seq, b64format.EncodeToString(e.sig))
+}
+
+func (e *rootEntry) sigHash() []byte {
+ h := sha3.NewLegacyKeccak256()
+ fmt.Fprintf(h, rootPrefix+" e=%s l=%s seq=%d", e.eroot, e.lroot, e.seq)
+ return h.Sum(nil)
+}
+
+func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool {
+ sig := e.sig[:crypto.RecoveryIDOffset] // remove recovery id
+ return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig)
+}
+
+func (e *subtreeEntry) String() string {
+ return "enrtree=" + strings.Join(e.children, ",")
+}
+
+func (e *enrEntry) String() string {
+ enc, _ := rlp.EncodeToBytes(e.node.Record())
+ return "enr=" + b64format.EncodeToString(enc)
+}
+
+func (e *linkEntry) String() string {
+ return "enrtree-link=" + e.link()
+}
+
+func (e *linkEntry) url() string {
+ return "enrtree://" + e.link()
+}
+
+func (e *linkEntry) link() string {
+ return fmt.Sprintf("%s@%s", b32format.EncodeToString(crypto.CompressPubkey(e.pubkey)), e.domain)
+}
+
+// Entry Parsing
+
+func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
+ switch {
+ case strings.HasPrefix(e, "enrtree-link="):
+ return parseLink(e[13:])
+ case strings.HasPrefix(e, "enrtree="):
+ return parseSubtree(e[8:])
+ case strings.HasPrefix(e, "enr="):
+ return parseENR(e[4:], validSchemes)
+ default:
+ return nil, errUnknownEntry
+ }
+}
+
+func parseRoot(e string) (rootEntry, error) {
+ var eroot, lroot, sig string
+ var seq uint
+ if _, err := fmt.Sscanf(e, rootPrefix+" e=%s l=%s seq=%d sig=%s", &eroot, &lroot, &seq, &sig); err != nil {
+ return rootEntry{}, entryError{"root", errSyntax}
+ }
+ if !isValidHash(eroot) || !isValidHash(lroot) {
+ return rootEntry{}, entryError{"root", errInvalidChild}
+ }
+ sigb, err := b64format.DecodeString(sig)
+ if err != nil || len(sigb) != crypto.SignatureLength {
+ return rootEntry{}, entryError{"root", errInvalidSig}
+ }
+ return rootEntry{eroot, lroot, seq, sigb}, nil
+}
+
+func parseLink(e string) (entry, error) {
+ pos := strings.IndexByte(e, '@')
+ if pos == -1 {
+ return nil, entryError{"link", errNoPubkey}
+ }
+ keystring, domain := e[:pos], e[pos+1:]
+ keybytes, err := b32format.DecodeString(keystring)
+ if err != nil {
+ return nil, entryError{"link", errBadPubkey}
+ }
+ key, err := crypto.DecompressPubkey(keybytes)
+ if err != nil {
+ return nil, entryError{"link", errBadPubkey}
+ }
+ return &linkEntry{domain, key}, nil
+}
+
+func parseSubtree(e string) (entry, error) {
+ if e == "" {
+ return &subtreeEntry{}, nil // empty entry is OK
+ }
+ hashes := make([]string, 0, strings.Count(e, ","))
+ for _, c := range strings.Split(e, ",") {
+ if !isValidHash(c) {
+ return nil, entryError{"subtree", errInvalidChild}
+ }
+ hashes = append(hashes, c)
+ }
+ return &subtreeEntry{hashes}, nil
+}
+
+func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) {
+ enc, err := b64format.DecodeString(e)
+ if err != nil {
+ return nil, entryError{"enr", errInvalidENR}
+ }
+ var rec enr.Record
+ if err := rlp.DecodeBytes(enc, &rec); err != nil {
+ return nil, entryError{"enr", err}
+ }
+ n, err := enode.New(validSchemes, &rec)
+ if err != nil {
+ return nil, entryError{"enr", err}
+ }
+ return &enrEntry{n}, nil
+}
+
+func isValidHash(s string) bool {
+ dlen := b32format.DecodedLen(len(s))
+ if dlen < minHashLength || dlen > 32 || strings.ContainsAny(s, "\n\r") {
+ return false
+ }
+ buf := make([]byte, 32)
+ _, err := b32format.Decode(buf, []byte(s))
+ return err == nil
+}
+
+// truncateHash truncates the given base32 hash string to the minimum acceptable length.
+func truncateHash(hash string) string {
+ maxLen := b32format.EncodedLen(minHashLength)
+ if len(hash) < maxLen {
+ panic(fmt.Errorf("dnsdisc: hash %q is too short", hash))
+ }
+ return hash[:maxLen]
+}
+
+// URL encoding
+
+// ParseURL parses an enrtree:// URL and returns its components.
+func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) {
+ le, err := parseURL(url)
+ if err != nil {
+ return "", nil, err
+ }
+ return le.domain, le.pubkey, nil
+}
+
+func parseURL(url string) (*linkEntry, error) {
+ const scheme = "enrtree://"
+ if !strings.HasPrefix(url, scheme) {
+ return nil, fmt.Errorf("wrong/missing scheme 'enrtree' in URL")
+ }
+ le, err := parseLink(url[len(scheme):])
+ if err != nil {
+ return nil, err.(entryError).err
+ }
+ return le.(*linkEntry), nil
+}
diff --git a/p2p/dnsdisc/tree_test.go b/p2p/dnsdisc/tree_test.go
new file mode 100644
index 0000000000..e2fe962452
--- /dev/null
+++ b/p2p/dnsdisc/tree_test.go
@@ -0,0 +1,144 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package dnsdisc
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+func TestParseRoot(t *testing.T) {
+ tests := []struct {
+ input string
+ e rootEntry
+ err error
+ }{
+ {
+ input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
+ err: entryError{"root", errSyntax},
+ },
+ {
+ input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
+ err: entryError{"root", errInvalidSig},
+ },
+ {
+ input: "enrtree-root=v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE=",
+ e: rootEntry{
+ eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU",
+ lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A",
+ seq: 3,
+ sig: hexutil.MustDecode("0xdc5997b95c296bc63b3acb594f1f4f21bd66b7c16b5bb5690ce16fe006860ac6761081e686b69685ee0dc588500e5c393237855d831b263b0f78a947ce62511101"),
+ },
+ },
+ }
+ for i, test := range tests {
+ e, err := parseRoot(test.input)
+ if !reflect.DeepEqual(e, test.e) {
+ t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
+ }
+ if err != test.err {
+ t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
+ }
+ }
+}
+
+func TestParseEntry(t *testing.T) {
+ testkey := testKey(signingKeySeed)
+ tests := []struct {
+ input string
+ e entry
+ err error
+ }{
+ // Subtrees:
+ {
+ input: "enrtree=1,2",
+ err: entryError{"subtree", errInvalidChild},
+ },
+ {
+ input: "enrtree=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ err: entryError{"subtree", errInvalidChild},
+ },
+ {
+ input: "enrtree=",
+ e: &subtreeEntry{},
+ },
+ {
+ input: "enrtree=AAAAAAAAAAAAAAAAAAAA",
+ e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}},
+ },
+ {
+ input: "enrtree=AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB",
+ e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}},
+ },
+ // Links
+ {
+ input: "enrtree-link=AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
+ e: &linkEntry{"nodes.example.org", &testkey.PublicKey},
+ },
+ {
+ input: "enrtree-link=nodes.example.org",
+ err: entryError{"link", errNoPubkey},
+ },
+ {
+ input: "enrtree-link=AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org",
+ err: entryError{"link", errBadPubkey},
+ },
+ {
+ input: "enrtree-link=AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org",
+ err: entryError{"link", errBadPubkey},
+ },
+ // ENRs
+ {
+ input: "enr=-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM=",
+ e: &enrEntry{node: testNode(nodesSeed1)},
+ },
+ {
+ input: "enr=-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=",
+ err: entryError{"enr", errInvalidENR},
+ },
+ // Invalid:
+ {input: "", err: errUnknownEntry},
+ {input: "foo", err: errUnknownEntry},
+ {input: "enrtree", err: errUnknownEntry},
+ {input: "enrtree-x=", err: errUnknownEntry},
+ }
+ for i, test := range tests {
+ e, err := parseEntry(test.input, enode.ValidSchemes)
+ if !reflect.DeepEqual(e, test.e) {
+ t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
+ }
+ if err != test.err {
+ t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
+ }
+ }
+}
+
+func TestMakeTree(t *testing.T) {
+ nodes := testNodes(nodesSeed2, 50)
+ tree, err := MakeTree(2, nodes, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ txt := tree.ToTXT("")
+ if len(txt) < len(nodes)+1 {
+ t.Fatal("too few TXT records in output")
+ }
+}