go-pulse/p2p/dnsdisc/client_test.go
Felix Lange 57d4898e29
p2p/dnsdisc: re-check tree root when leaf resolution fails (#20682)
This adds additional logic to re-resolve the root name of a tree when a
couple of leaf requests have failed. We need this change to avoid
getting into a failure state where leaf requests keep failing for half
an hour when the tree has been updated.
2020-02-17 15:23:25 +01:00

398 lines
12 KiB
Go

// 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"
"errors"
"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{
"n": "enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA",
"C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"JWXYDBPXYWG6FX3GMDIBFA6CJ4.n": "enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24",
"2XS2367YHAXJFGLZHVAWLQD4ZY.n": "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA",
"H4FHT4B454P6UXFD7JCYQ5PWDY.n": "enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI",
"MHTDO6TMUBRIA2XWG5LUDACK24.n": "enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o",
}
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())
}
}
// In this test, syncing the tree fails because it contains an invalid ENR entry.
func TestClientSyncTreeBadNode(t *testing.T) {
// var b strings.Builder
// b.WriteString(enrPrefix)
// b.WriteString("-----")
// badHash := subdomain(&b)
// tree, _ := MakeTree(3, nil, []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"})
// tree.entries[badHash] = &b
// tree.root.eroot = badHash
// url, _ := tree.Sign(testKey(signingKeySeed), "n")
// fmt.Println(url)
// fmt.Printf("%#v\n", tree.ToTXT("n"))
r := mapResolver{
"n": "enrtree-root:v1 e=INDMVBZEEQ4ESVYAKGIYU74EAA l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=3 sig=Vl3AmunLur0JZ3sIyJPSH6A3Vvdp4F40jWQeCmkIhmcgwE4VC5U9wpK8C_uL_CMY29fd6FAhspRvq2z_VysTLAA",
"C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"INDMVBZEEQ4ESVYAKGIYU74EAA.n": "enr:-----",
}
c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
_, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
wantErr := nameError{name: "INDMVBZEEQ4ESVYAKGIYU74EAA.n", err: entryError{typ: "enr", err: errInvalidENR}}
if err != wantErr {
t.Fatalf("expected sync error %q, got %q", wantErr, err)
}
}
// This test checks that randomIterator finds all entries.
func TestIterator(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),
RateLimit: 500,
})
it, err := c.NewIterator(url)
if err != nil {
t.Fatal(err)
}
checkIterator(t, it, nodes)
}
// This test checks if closing randomIterator races.
func TestIteratorClose(t *testing.T) {
nodes := testNodes(nodesSeed1, 500)
tree1, url1 := makeTestTree("t1", nodes, nil)
c := NewClient(Config{Resolver: newMapResolver(tree1.ToTXT("t1"))})
it, err := c.NewIterator(url1)
if err != nil {
t.Fatal(err)
}
done := make(chan struct{})
go func() {
for it.Next() {
_ = it.Node()
}
close(done)
}()
time.Sleep(50 * time.Millisecond)
it.Close()
<-done
}
// This test checks that randomIterator traverses linked trees as well as explicitly added trees.
func TestIteratorLinks(t *testing.T) {
nodes := testNodes(nodesSeed1, 40)
tree1, url1 := makeTestTree("t1", nodes[:10], nil)
tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1})
c := NewClient(Config{
Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
Logger: testlog.Logger(t, log.LvlTrace),
RateLimit: 500,
})
it, err := c.NewIterator(url2)
if err != nil {
t.Fatal(err)
}
checkIterator(t, it, nodes)
}
// This test verifies that randomIterator re-checks the root of the tree to catch
// updates to nodes.
func TestIteratorNodeUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
c = NewClient(Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
RateLimit: 500,
})
)
c.clock = clock
tree1, url := makeTestTree("n", nodes[:25], nil)
it, err := c.NewIterator(url)
if err != nil {
t.Fatal(err)
}
// Sync the original tree.
resolver.add(tree1.ToTXT("n"))
checkIterator(t, it, nodes[:25])
// Ensure RandomNode returns the new nodes after the tree is updated.
updateSomeNodes(nodesSeed1, nodes)
tree2, _ := makeTestTree("n", nodes, nil)
resolver.clear()
resolver.add(tree2.ToTXT("n"))
t.Log("tree updated")
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
checkIterator(t, it, nodes)
}
// This test checks that the tree root is rechecked when a couple of leaf
// requests have failed. The test is just like TestIteratorNodeUpdates, but
// without advancing the clock by recheckInterval after the tree update.
func TestIteratorRootRecheckOnFail(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
c = NewClient(Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
RateLimit: 500,
// Disabling the cache is required for this test because the client doesn't
// notice leaf failures if all records are cached.
CacheLimit: 1,
})
)
c.clock = clock
tree1, url := makeTestTree("n", nodes[:25], nil)
it, err := c.NewIterator(url)
if err != nil {
t.Fatal(err)
}
// Sync the original tree.
resolver.add(tree1.ToTXT("n"))
checkIterator(t, it, nodes[:25])
// Ensure RandomNode returns the new nodes after the tree is updated.
updateSomeNodes(nodesSeed1, nodes)
tree2, _ := makeTestTree("n", nodes, nil)
resolver.clear()
resolver.add(tree2.ToTXT("n"))
t.Log("tree updated")
checkIterator(t, it, nodes)
}
// updateSomeNodes applies ENR updates to some of the given nodes.
func updateSomeNodes(keySeed int64, nodes []*enode.Node) {
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
}
}
// This test verifies that randomIterator re-checks the root of the tree to catch
// updates to links.
func TestIteratorLinkUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
c = NewClient(Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
RateLimit: 500,
})
)
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"))
it, err := c.NewIterator(url1)
if err != nil {
t.Fatal(err)
}
// Sync tree1 using RandomNode.
checkIterator(t, it, nodes[:20])
// Add link to tree3, remove link to tree2.
tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
resolver.add(tree1.ToTXT("t1"))
t.Log("tree1 updated")
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
var wantNodes []*enode.Node
wantNodes = append(wantNodes, tree1.Nodes()...)
wantNodes = append(wantNodes, tree3.Nodes()...)
checkIterator(t, it, wantNodes)
// Check that linked trees are GCed when they're no longer referenced.
knownTrees := it.(*randomIterator).trees
if len(knownTrees) != 2 {
t.Errorf("client knows %d trees, want 2", len(knownTrees))
}
}
func checkIterator(t *testing.T, it enode.Iterator, wantNodes []*enode.Node) {
t.Helper()
var (
want = make(map[enode.ID]*enode.Node)
maxCalls = len(wantNodes) * 3
calls = 0
)
for _, n := range wantNodes {
want[n.ID()] = n
}
for ; len(want) > 0 && calls < maxCalls; calls++ {
if !it.Next() {
t.Fatalf("Next returned false (call %d)", calls)
}
n := it.Node()
delete(want, n.ID())
}
t.Logf("checkIterator called Next %d times to find %d nodes", calls, len(wantNodes))
for _, n := range want {
t.Errorf("iterator 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, errors.New("not found")
}