mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
p2p/dnsdisc: add implementation of EIP-1459
This adds an implementation of node discovery via DNS TXT records to the go-ethereum library. The implementation doesn't match EIP-1459 exactly, the main difference being that the implementation uses separate merkle trees for tree links and ENRs. The EIP will be updated to match this implementation.
This commit is contained in:
parent
24ef83518c
commit
e709788840
7 changed files with 1452 additions and 0 deletions
260
p2p/dnsdisc/client.go
Normal file
260
p2p/dnsdisc/client.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
306
p2p/dnsdisc/client_test.go
Normal file
306
p2p/dnsdisc/client_test.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
18
p2p/dnsdisc/doc.go
Normal file
18
p2p/dnsdisc/doc.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Package dnsdisc implements node discovery via DNS (EIP-1459).
|
||||||
|
package dnsdisc
|
||||||
63
p2p/dnsdisc/error.go
Normal file
63
p2p/dnsdisc/error.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
277
p2p/dnsdisc/sync.go
Normal file
277
p2p/dnsdisc/sync.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
384
p2p/dnsdisc/tree.go
Normal file
384
p2p/dnsdisc/tree.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
144
p2p/dnsdisc/tree_test.go
Normal file
144
p2p/dnsdisc/tree_test.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue