p2p/dnsdisc: update to latest EIP-1459 spec (#20168)

This updates the DNS TXT record format to the latest
changes in ethereum/EIPs#2313.
This commit is contained in:
Felix Lange 2019-10-16 13:35:24 +02:00 committed by Péter Szilágyi
parent c476460cb2
commit 7300365956
5 changed files with 92 additions and 81 deletions

View File

@ -107,7 +107,7 @@ func NewClient(cfg Config, urls ...string) (*Client, error) {
// SyncTree downloads the entire node tree at the given URL. This doesn't add the tree for // 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. // later use, but any previously-synced entries are reused.
func (c *Client) SyncTree(url string) (*Tree, error) { func (c *Client) SyncTree(url string) (*Tree, error) {
le, err := parseURL(url) le, err := parseLink(url)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid enrtree URL: %v", err) return nil, fmt.Errorf("invalid enrtree URL: %v", err)
} }
@ -122,7 +122,7 @@ func (c *Client) SyncTree(url string) (*Tree, error) {
// AddTree adds a enrtree:// URL to crawl. // AddTree adds a enrtree:// URL to crawl.
func (c *Client) AddTree(url string) error { func (c *Client) AddTree(url string) error {
le, err := parseURL(url) le, err := parseLink(url)
if err != nil { if err != nil {
return fmt.Errorf("invalid enrtree URL: %v", err) return fmt.Errorf("invalid enrtree URL: %v", err)
} }

View File

@ -41,12 +41,12 @@ const (
func TestClientSyncTree(t *testing.T) { func TestClientSyncTree(t *testing.T) {
r := mapResolver{ r := mapResolver{
"3CA2MBMUQ55ZCT74YEEQLANJDI.n": "enr=-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI=", "n": "enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA",
"53HBTPGGZ4I76UEPCNQGZWIPTQ.n": "enr=-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA=", "C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"BG7SVUBUAJ3UAWD2ATEBLMRNEE.n": "enrtree=53HBTPGGZ4I76UEPCNQGZWIPTQ,3CA2MBMUQ55ZCT74YEEQLANJDI,HNHR6UTVZF5TJKK3FV27ZI76P4", "JWXYDBPXYWG6FX3GMDIBFA6CJ4.n": "enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24",
"HNHR6UTVZF5TJKK3FV27ZI76P4.n": "enr=-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o=", "2XS2367YHAXJFGLZHVAWLQD4ZY.n": "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA",
"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org", "H4FHT4B454P6UXFD7JCYQ5PWDY.n": "enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI",
"n": "enrtree-root=v1 e=BG7SVUBUAJ3UAWD2ATEBLMRNEE l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=1 sig=gacuU0nTy9duIdu1IFDyF5Lv9CFHqHiNcj91n0frw70tZo3tZZsCVkE3j1ILYyVOHRLWGBmawo_SEkThZ9PgcQE=", "MHTDO6TMUBRIA2XWG5LUDACK24.n": "enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o",
} }
var ( var (
wantNodes = testNodes(0x29452, 3) wantNodes = testNodes(0x29452, 3)
@ -75,15 +75,25 @@ func TestClientSyncTree(t *testing.T) {
// In this test, syncing the tree fails because it contains an invalid ENR entry. // In this test, syncing the tree fails because it contains an invalid ENR entry.
func TestClientSyncTreeBadNode(t *testing.T) { func TestClientSyncTreeBadNode(t *testing.T) {
r := mapResolver{ // var b strings.Builder
"n": "enrtree-root=v1 e=ZFJZDQKSOMJRYYQSZKJZC54HCF l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=WEy8JTZ2dHmXM2qeBZ7D2ECK7SGbnurl1ge_S_5GQBAqnADk0gLTcg8Lm5QNqLHZjJKGAb443p996idlMcBqEQA=", // b.WriteString(enrPrefix)
"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org", // b.WriteString("-----")
"ZFJZDQKSOMJRYYQSZKJZC54HCF.n": "enr=gggggggggggggg=", // 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)}) c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
_, err := c.SyncTree("enrtree://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@n") _, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
wantErr := nameError{name: "ZFJZDQKSOMJRYYQSZKJZC54HCF.n", err: entryError{typ: "enr", err: errInvalidENR}} wantErr := nameError{name: "INDMVBZEEQ4ESVYAKGIYU74EAA.n", err: entryError{typ: "enr", err: errInvalidENR}}
if err != wantErr { if err != wantErr {
t.Fatalf("expected sync error %q, got %q", wantErr, err) t.Fatalf("expected sync error %q, got %q", wantErr, err)
} }

View File

@ -120,7 +120,7 @@ func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error
} }
func (ct *clientTree) String() string { func (ct *clientTree) String() string {
return ct.loc.url() return ct.loc.String()
} }
// removeHash removes the element at index from h. // removeHash removes the element at index from h.
@ -209,7 +209,7 @@ func (ts *subtreeSync) resolveNext(ctx context.Context, hash string) (entry, err
if !ts.link { if !ts.link {
return nil, errLinkInENRTree return nil, errLinkInENRTree
} }
case *subtreeEntry: case *branchEntry:
ts.missing = append(ts.missing, e.children...) ts.missing = append(ts.missing, e.children...)
} }
return e, nil return e, nil

View File

@ -49,7 +49,7 @@ func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error
root.sig = sig root.sig = sig
t.root = &root t.root = &root
link := &linkEntry{domain, &key.PublicKey} link := &linkEntry{domain, &key.PublicKey}
return link.url(), nil return link.String(), nil
} }
// SetSignature verifies the given signature and assigns it as the tree's current // SetSignature verifies the given signature and assigns it as the tree's current
@ -96,7 +96,7 @@ func (t *Tree) Links() []string {
var links []string var links []string
for _, e := range t.entries { for _, e := range t.entries {
if le, ok := e.(*linkEntry); ok { if le, ok := e.(*linkEntry); ok {
links = append(links, le.url()) links = append(links, le.String())
} }
} }
return links return links
@ -115,15 +115,15 @@ func (t *Tree) Nodes() []*enode.Node {
const ( const (
hashAbbrev = 16 hashAbbrev = 16
maxChildren = 300 / (hashAbbrev * (13 / 8)) maxChildren = 300 / hashAbbrev * (13 / 8)
minHashLength = 12 minHashLength = 12
rootPrefix = "enrtree-root=v1"
) )
// MakeTree creates a tree containing the given nodes and links. // MakeTree creates a tree containing the given nodes and links.
func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) { func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
// Sort records by ID and ensure all nodes have a valid record. // Sort records by ID and ensure all nodes have a valid record.
records := make([]*enode.Node, len(nodes)) records := make([]*enode.Node, len(nodes))
copy(records, nodes) copy(records, nodes)
sortByID(records) sortByID(records)
for _, n := range records { for _, n := range records {
@ -139,7 +139,7 @@ func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
} }
linkEntries := make([]entry, len(links)) linkEntries := make([]entry, len(links))
for i, l := range links { for i, l := range links {
le, err := parseURL(l) le, err := parseLink(l)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -166,7 +166,7 @@ func (t *Tree) build(entries []entry) entry {
hashes[i] = subdomain(e) hashes[i] = subdomain(e)
t.entries[hashes[i]] = e t.entries[hashes[i]] = e
} }
return &subtreeEntry{hashes} return &branchEntry{hashes}
} }
var subtrees []entry var subtrees []entry
for len(entries) > 0 { for len(entries) > 0 {
@ -202,7 +202,7 @@ type (
seq uint seq uint
sig []byte sig []byte
} }
subtreeEntry struct { branchEntry struct {
children []string children []string
} }
enrEntry struct { enrEntry struct {
@ -218,7 +218,14 @@ type (
var ( var (
b32format = base32.StdEncoding.WithPadding(base32.NoPadding) b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
b64format = base64.URLEncoding b64format = base64.RawURLEncoding
)
const (
rootPrefix = "enrtree-root:v1"
linkPrefix = "enrtree://"
branchPrefix = "enrtree-branch:"
enrPrefix = "enr:"
) )
func subdomain(e entry) string { func subdomain(e entry) string {
@ -242,37 +249,29 @@ func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool {
return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig) return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig)
} }
func (e *subtreeEntry) String() string { func (e *branchEntry) String() string {
return "enrtree=" + strings.Join(e.children, ",") return branchPrefix + strings.Join(e.children, ",")
} }
func (e *enrEntry) String() string { func (e *enrEntry) String() string {
enc, _ := rlp.EncodeToBytes(e.node.Record()) return e.node.String()
return "enr=" + b64format.EncodeToString(enc)
} }
func (e *linkEntry) String() string { func (e *linkEntry) String() string {
return "enrtree-link=" + e.link() pubkey := b32format.EncodeToString(crypto.CompressPubkey(e.pubkey))
} return fmt.Sprintf("%s%s@%s", linkPrefix, pubkey, e.domain)
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 // Entry Parsing
func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) { func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
switch { switch {
case strings.HasPrefix(e, "enrtree-link="): case strings.HasPrefix(e, linkPrefix):
return parseLink(e[13:]) return parseLinkEntry(e)
case strings.HasPrefix(e, "enrtree="): case strings.HasPrefix(e, branchPrefix):
return parseSubtree(e[8:]) return parseBranch(e)
case strings.HasPrefix(e, "enr="): case strings.HasPrefix(e, enrPrefix):
return parseENR(e[4:], validSchemes) return parseENR(e, validSchemes)
default: default:
return nil, errUnknownEntry return nil, errUnknownEntry
} }
@ -294,7 +293,19 @@ func parseRoot(e string) (rootEntry, error) {
return rootEntry{eroot, lroot, seq, sigb}, nil return rootEntry{eroot, lroot, seq, sigb}, nil
} }
func parseLink(e string) (entry, error) { func parseLinkEntry(e string) (entry, error) {
le, err := parseLink(e)
if err != nil {
return nil, err
}
return le, nil
}
func parseLink(e string) (*linkEntry, error) {
if !strings.HasPrefix(e, linkPrefix) {
return nil, fmt.Errorf("wrong/missing scheme 'enrtree' in URL")
}
e = e[len(linkPrefix):]
pos := strings.IndexByte(e, '@') pos := strings.IndexByte(e, '@')
if pos == -1 { if pos == -1 {
return nil, entryError{"link", errNoPubkey} return nil, entryError{"link", errNoPubkey}
@ -311,21 +322,23 @@ func parseLink(e string) (entry, error) {
return &linkEntry{domain, key}, nil return &linkEntry{domain, key}, nil
} }
func parseSubtree(e string) (entry, error) { func parseBranch(e string) (entry, error) {
e = e[len(branchPrefix):]
if e == "" { if e == "" {
return &subtreeEntry{}, nil // empty entry is OK return &branchEntry{}, nil // empty entry is OK
} }
hashes := make([]string, 0, strings.Count(e, ",")) hashes := make([]string, 0, strings.Count(e, ","))
for _, c := range strings.Split(e, ",") { for _, c := range strings.Split(e, ",") {
if !isValidHash(c) { if !isValidHash(c) {
return nil, entryError{"subtree", errInvalidChild} return nil, entryError{"branch", errInvalidChild}
} }
hashes = append(hashes, c) hashes = append(hashes, c)
} }
return &subtreeEntry{hashes}, nil return &branchEntry{hashes}, nil
} }
func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) { func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) {
e = e[len(enrPrefix):]
enc, err := b64format.DecodeString(e) enc, err := b64format.DecodeString(e)
if err != nil { if err != nil {
return nil, entryError{"enr", errInvalidENR} return nil, entryError{"enr", errInvalidENR}
@ -364,21 +377,9 @@ func truncateHash(hash string) string {
// ParseURL parses an enrtree:// URL and returns its components. // ParseURL parses an enrtree:// URL and returns its components.
func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) { func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) {
le, err := parseURL(url) le, err := parseLink(url)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
return le.domain, le.pubkey, nil 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
}

View File

@ -32,15 +32,15 @@ func TestParseRoot(t *testing.T) {
err error err error
}{ }{
{ {
input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=", input: "enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw",
err: entryError{"root", errSyntax}, err: entryError{"root", errSyntax},
}, },
{ {
input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=", input: "enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw",
err: entryError{"root", errInvalidSig}, err: entryError{"root", errInvalidSig},
}, },
{ {
input: "enrtree-root=v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE=", input: "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE",
e: rootEntry{ e: rootEntry{
eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU", eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU",
lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A", lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A",
@ -69,49 +69,49 @@ func TestParseEntry(t *testing.T) {
}{ }{
// Subtrees: // Subtrees:
{ {
input: "enrtree=1,2", input: "enrtree-branch:1,2",
err: entryError{"subtree", errInvalidChild}, err: entryError{"branch", errInvalidChild},
}, },
{ {
input: "enrtree=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
err: entryError{"subtree", errInvalidChild}, err: entryError{"branch", errInvalidChild},
}, },
{ {
input: "enrtree=", input: "enrtree-branch:",
e: &subtreeEntry{}, e: &branchEntry{},
}, },
{ {
input: "enrtree=AAAAAAAAAAAAAAAAAAAA", input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA",
e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}}, e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}},
}, },
{ {
input: "enrtree=AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB", input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB",
e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}}, e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}},
}, },
// Links // Links
{ {
input: "enrtree-link=AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org", input: "enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
e: &linkEntry{"nodes.example.org", &testkey.PublicKey}, e: &linkEntry{"nodes.example.org", &testkey.PublicKey},
}, },
{ {
input: "enrtree-link=nodes.example.org", input: "enrtree://nodes.example.org",
err: entryError{"link", errNoPubkey}, err: entryError{"link", errNoPubkey},
}, },
{ {
input: "enrtree-link=AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org", input: "enrtree://AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org",
err: entryError{"link", errBadPubkey}, err: entryError{"link", errBadPubkey},
}, },
{ {
input: "enrtree-link=AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org", input: "enrtree://AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org",
err: entryError{"link", errBadPubkey}, err: entryError{"link", errBadPubkey},
}, },
// ENRs // ENRs
{ {
input: "enr=-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM=", input: "enr:-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM",
e: &enrEntry{node: testNode(nodesSeed1)}, e: &enrEntry{node: testNode(nodesSeed1)},
}, },
{ {
input: "enr=-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=", input: "enr:-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=",
err: entryError{"enr", errInvalidENR}, err: entryError{"enr", errInvalidENR},
}, },
// Invalid: // Invalid: