diff --git a/p2p/dnsdisc/client.go b/p2p/dnsdisc/client.go index d4ed08ecb..677c0aa92 100644 --- a/p2p/dnsdisc/client.go +++ b/p2p/dnsdisc/client.go @@ -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 // later use, but any previously-synced entries are reused. func (c *Client) SyncTree(url string) (*Tree, error) { - le, err := parseURL(url) + le, err := parseLink(url) if err != nil { 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. func (c *Client) AddTree(url string) error { - le, err := parseURL(url) + le, err := parseLink(url) if err != nil { return fmt.Errorf("invalid enrtree URL: %v", err) } diff --git a/p2p/dnsdisc/client_test.go b/p2p/dnsdisc/client_test.go index 7e3a0f482..d8e3ecee3 100644 --- a/p2p/dnsdisc/client_test.go +++ b/p2p/dnsdisc/client_test.go @@ -41,12 +41,12 @@ const ( 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=", + "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) @@ -75,15 +75,25 @@ func TestClientSyncTree(t *testing.T) { // 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=", - } + // 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://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@n") - wantErr := nameError{name: "ZFJZDQKSOMJRYYQSZKJZC54HCF.n", err: entryError{typ: "enr", err: errInvalidENR}} + _, 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) } diff --git a/p2p/dnsdisc/sync.go b/p2p/dnsdisc/sync.go index bd5c8d023..533dacc65 100644 --- a/p2p/dnsdisc/sync.go +++ b/p2p/dnsdisc/sync.go @@ -120,7 +120,7 @@ func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error } func (ct *clientTree) String() string { - return ct.loc.url() + return ct.loc.String() } // 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 { return nil, errLinkInENRTree } - case *subtreeEntry: + case *branchEntry: ts.missing = append(ts.missing, e.children...) } return e, nil diff --git a/p2p/dnsdisc/tree.go b/p2p/dnsdisc/tree.go index 855d4968c..eba2ff9c0 100644 --- a/p2p/dnsdisc/tree.go +++ b/p2p/dnsdisc/tree.go @@ -49,7 +49,7 @@ func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error root.sig = sig t.root = &root 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 @@ -96,7 +96,7 @@ func (t *Tree) Links() []string { var links []string for _, e := range t.entries { if le, ok := e.(*linkEntry); ok { - links = append(links, le.url()) + links = append(links, le.String()) } } return links @@ -115,15 +115,15 @@ func (t *Tree) Nodes() []*enode.Node { const ( hashAbbrev = 16 - maxChildren = 300 / (hashAbbrev * (13 / 8)) + 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 { @@ -139,7 +139,7 @@ func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) { } linkEntries := make([]entry, len(links)) for i, l := range links { - le, err := parseURL(l) + le, err := parseLink(l) if err != nil { return nil, err } @@ -166,7 +166,7 @@ func (t *Tree) build(entries []entry) entry { hashes[i] = subdomain(e) t.entries[hashes[i]] = e } - return &subtreeEntry{hashes} + return &branchEntry{hashes} } var subtrees []entry for len(entries) > 0 { @@ -202,7 +202,7 @@ type ( seq uint sig []byte } - subtreeEntry struct { + branchEntry struct { children []string } enrEntry struct { @@ -218,7 +218,14 @@ type ( var ( 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 { @@ -242,37 +249,29 @@ func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool { return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig) } -func (e *subtreeEntry) String() string { - return "enrtree=" + strings.Join(e.children, ",") +func (e *branchEntry) String() string { + return branchPrefix + strings.Join(e.children, ",") } func (e *enrEntry) String() string { - enc, _ := rlp.EncodeToBytes(e.node.Record()) - return "enr=" + b64format.EncodeToString(enc) + return e.node.String() } 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) + pubkey := b32format.EncodeToString(crypto.CompressPubkey(e.pubkey)) + return fmt.Sprintf("%s%s@%s", linkPrefix, 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) + case strings.HasPrefix(e, linkPrefix): + return parseLinkEntry(e) + case strings.HasPrefix(e, branchPrefix): + return parseBranch(e) + case strings.HasPrefix(e, enrPrefix): + return parseENR(e, validSchemes) default: return nil, errUnknownEntry } @@ -294,7 +293,19 @@ func parseRoot(e string) (rootEntry, error) { 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, '@') if pos == -1 { return nil, entryError{"link", errNoPubkey} @@ -311,21 +322,23 @@ func parseLink(e string) (entry, error) { return &linkEntry{domain, key}, nil } -func parseSubtree(e string) (entry, error) { +func parseBranch(e string) (entry, error) { + e = e[len(branchPrefix):] if e == "" { - return &subtreeEntry{}, nil // empty entry is OK + return &branchEntry{}, 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} + return nil, entryError{"branch", errInvalidChild} } hashes = append(hashes, c) } - return &subtreeEntry{hashes}, nil + return &branchEntry{hashes}, nil } func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) { + e = e[len(enrPrefix):] enc, err := b64format.DecodeString(e) if err != nil { return nil, entryError{"enr", errInvalidENR} @@ -364,21 +377,9 @@ func truncateHash(hash string) string { // ParseURL parses an enrtree:// URL and returns its components. func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) { - le, err := parseURL(url) + le, err := parseLink(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 index e2fe96245..b6d0a8433 100644 --- a/p2p/dnsdisc/tree_test.go +++ b/p2p/dnsdisc/tree_test.go @@ -32,15 +32,15 @@ func TestParseRoot(t *testing.T) { 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}, }, { - 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}, }, { - 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{ eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU", lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A", @@ -69,49 +69,49 @@ func TestParseEntry(t *testing.T) { }{ // Subtrees: { - input: "enrtree=1,2", - err: entryError{"subtree", errInvalidChild}, + input: "enrtree-branch:1,2", + err: entryError{"branch", errInvalidChild}, }, { - input: "enrtree=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - err: entryError{"subtree", errInvalidChild}, + input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + err: entryError{"branch", errInvalidChild}, }, { - input: "enrtree=", - e: &subtreeEntry{}, + input: "enrtree-branch:", + e: &branchEntry{}, }, { - input: "enrtree=AAAAAAAAAAAAAAAAAAAA", - e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}}, + input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA", + e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}}, }, { - input: "enrtree=AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB", - e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}}, + input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB", + e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}}, }, // Links { - input: "enrtree-link=AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org", + input: "enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org", e: &linkEntry{"nodes.example.org", &testkey.PublicKey}, }, { - input: "enrtree-link=nodes.example.org", + input: "enrtree://nodes.example.org", err: entryError{"link", errNoPubkey}, }, { - input: "enrtree-link=AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org", + input: "enrtree://AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org", err: entryError{"link", errBadPubkey}, }, { - input: "enrtree-link=AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org", + input: "enrtree://AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org", err: entryError{"link", errBadPubkey}, }, // ENRs { - input: "enr=-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM=", + input: "enr:-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM", e: &enrEntry{node: testNode(nodesSeed1)}, }, { - input: "enr=-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=", + input: "enr:-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=", err: entryError{"enr", errInvalidENR}, }, // Invalid: