Browse Source

eth协议监听添加进来了

skyfffire 2 năm trước cách đây
mục cha
commit
04e77c9b43
8 tập tin đã thay đổi với 1295 bổ sung8 xóa
  1. 5 1
      go.mod
  2. 67 0
      go.sum
  3. 386 0
      p2p/dnsdisc/client.go
  4. 18 0
      p2p/dnsdisc/doc.go
  5. 63 0
      p2p/dnsdisc/error.go
  6. 329 0
      p2p/dnsdisc/sync.go
  7. 423 0
      p2p/dnsdisc/tree.go
  8. 4 7
      p2p/server.go

+ 5 - 1
go.mod

@@ -7,11 +7,14 @@ require (
 	github.com/ethereum/go-ethereum v1.11.5
 	github.com/go-stack/stack v1.8.1
 	github.com/golang/snappy v0.0.4
+	github.com/hashicorp/golang-lru/v2 v2.0.2
 	github.com/huin/goupnp v1.0.3
 	github.com/jackpal/go-nat-pmp v1.0.2
 	github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
 	github.com/panjf2000/ants/v2 v2.7.2
 	golang.org/x/crypto v0.7.0
+	golang.org/x/sync v0.1.0
+	golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
 )
 
 require (
@@ -20,10 +23,11 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
 	github.com/go-ole/go-ole v1.2.1 // indirect
+	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/holiman/uint256 v1.2.0 // indirect
 	github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
+	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
 	github.com/tklauser/go-sysconf v0.3.5 // indirect
 	github.com/tklauser/numcpus v0.2.2 // indirect
-	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.6.0 // indirect
 )

+ 67 - 0
go.sum

@@ -19,20 +19,46 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
 github.com/ethereum/go-ethereum v1.11.5 h1:3M1uan+LAUvdn+7wCEFrcMM4LJTeuxDrPTg/f31a5QQ=
 github.com/ethereum/go-ethereum v1.11.5/go.mod h1:it7x0DWnTDMfVFdXcU6Ti4KEFQynLHVRarcSlPr0HBo=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
 github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
 github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
 github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
 github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
 github.com/panjf2000/ants/v2 v2.7.2 h1:2NUt9BaZFO5kQzrieOmK/wdb/tQ/K+QHaxN8sOgD63U=
@@ -49,20 +75,61 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
 github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
 github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
 github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
 github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
+golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 386 - 0
p2p/dnsdisc/client.go

@@ -0,0 +1,386 @@
+// 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"
+	"sync"
+	"time"
+
+	"blockchain-go/common/mclock"
+	"blockchain-go/log"
+	"blockchain-go/p2p/enode"
+	"blockchain-go/p2p/enr"
+	"github.com/ethereum/go-ethereum/crypto"
+	lru "github.com/hashicorp/golang-lru"
+	"golang.org/x/sync/singleflight"
+	"golang.org/x/time/rate"
+)
+
+// Client discovers nodes by querying DNS servers.
+type Client struct {
+	cfg          Config
+	clock        mclock.Clock
+	entries      *lru.Cache
+	ratelimit    *rate.Limiter
+	singleflight singleflight.Group
+}
+
+// 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)
+	RateLimit       float64            // maximum DNS requests / second (default 3)
+	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
+		defaultRateLimit = 3
+		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.RateLimit == 0 {
+		cfg.RateLimit = defaultRateLimit
+	}
+	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) *Client {
+	cfg = cfg.withDefaults()
+	cache, err := lru.New(cfg.CacheLimit)
+	if err != nil {
+		panic(err)
+	}
+	rlimit := rate.NewLimiter(rate.Limit(cfg.RateLimit), 10)
+	return &Client{
+		cfg:       cfg,
+		entries:   cache,
+		clock:     mclock.System{},
+		ratelimit: rlimit,
+	}
+}
+
+// SyncTree downloads the entire node tree at the given URL.
+func (c *Client) SyncTree(url string) (*Tree, error) {
+	le, err := parseLink(url)
+	if err != nil {
+		return nil, fmt.Errorf("invalid enrtree URL: %v", err)
+	}
+	ct := newClientTree(c, new(linkCache), 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
+}
+
+// NewIterator creates an iterator that visits all nodes at the
+// given tree URLs.
+func (c *Client) NewIterator(urls ...string) (enode.Iterator, error) {
+	it := c.newRandomIterator()
+	for _, url := range urls {
+		if err := it.addTree(url); err != nil {
+			return nil, err
+		}
+	}
+	return it, nil
+}
+
+// resolveRoot retrieves a root entry via DNS.
+func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
+	e, err, _ := c.singleflight.Do(loc.str, func() (interface{}, 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}
+	})
+	return e.(rootEntry), err
+}
+
+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) {
+	// The rate limit always applies, even when the result might be cached. This is
+	// important because it avoids hot-spinning in consumers of node iterators created on
+	// this client.
+	if err := c.ratelimit.Wait(ctx); err != nil {
+		return nil, err
+	}
+	cacheKey := truncateHash(hash)
+	if e, ok := c.entries.Get(cacheKey); ok {
+		return e.(entry), nil
+	}
+
+	ei, err, _ := c.singleflight.Do(cacheKey, func() (interface{}, error) {
+		e, err := c.doResolveEntry(ctx, domain, hash)
+		if err != nil {
+			return nil, err
+		}
+		c.entries.Add(cacheKey, e)
+		return e, nil
+	})
+	e, _ := ei.(entry)
+	return e, err
+}
+
+// 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}
+}
+
+// randomIterator traverses a set of trees and returns nodes found in them.
+type randomIterator struct {
+	cur      *enode.Node
+	ctx      context.Context
+	cancelFn context.CancelFunc
+	c        *Client
+
+	mu    sync.Mutex
+	lc    linkCache              // tracks tree dependencies
+	trees map[string]*clientTree // all trees
+	// buffers for syncableTrees
+	syncableList []*clientTree
+	disabledList []*clientTree
+}
+
+func (c *Client) newRandomIterator() *randomIterator {
+	ctx, cancel := context.WithCancel(context.Background())
+	return &randomIterator{
+		c:        c,
+		ctx:      ctx,
+		cancelFn: cancel,
+		trees:    make(map[string]*clientTree),
+	}
+}
+
+// Node returns the current node.
+func (it *randomIterator) Node() *enode.Node {
+	return it.cur
+}
+
+// Close closes the iterator.
+func (it *randomIterator) Close() {
+	it.cancelFn()
+
+	it.mu.Lock()
+	defer it.mu.Unlock()
+	it.trees = nil
+}
+
+// Next moves the iterator to the next node.
+func (it *randomIterator) Next() bool {
+	it.cur = it.nextNode()
+	return it.cur != nil
+}
+
+// addTree adds an enrtree:// URL to the iterator.
+func (it *randomIterator) addTree(url string) error {
+	le, err := parseLink(url)
+	if err != nil {
+		return fmt.Errorf("invalid enrtree URL: %v", err)
+	}
+	it.lc.addLink("", le.str)
+	return nil
+}
+
+// nextNode syncs random tree entries until it finds a node.
+func (it *randomIterator) nextNode() *enode.Node {
+	for {
+		ct := it.pickTree()
+		if ct == nil {
+			return nil
+		}
+		n, err := ct.syncRandom(it.ctx)
+		if err != nil {
+			if err == it.ctx.Err() {
+				return nil // context canceled.
+			}
+			it.c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
+			continue
+		}
+		if n != nil {
+			return n
+		}
+	}
+}
+
+// pickTree returns a random tree to sync from.
+func (it *randomIterator) pickTree() *clientTree {
+	it.mu.Lock()
+	defer it.mu.Unlock()
+
+	// Rebuild the trees map if any links have changed.
+	if it.lc.changed {
+		it.rebuildTrees()
+		it.lc.changed = false
+	}
+
+	for {
+		canSync, trees := it.syncableTrees()
+		switch {
+		case canSync:
+			// Pick a random tree.
+			return trees[rand.Intn(len(trees))]
+		case len(trees) > 0:
+			// No sync action can be performed on any tree right now. The only meaningful
+			// thing to do is waiting for any root record to get updated.
+			if !it.waitForRootUpdates(trees) {
+				// Iterator was closed while waiting.
+				return nil
+			}
+		default:
+			// There are no trees left, the iterator was closed.
+			return nil
+		}
+	}
+}
+
+// syncableTrees finds trees on which any meaningful sync action can be performed.
+func (it *randomIterator) syncableTrees() (canSync bool, trees []*clientTree) {
+	// Resize tree lists.
+	it.syncableList = it.syncableList[:0]
+	it.disabledList = it.disabledList[:0]
+
+	// Partition them into the two lists.
+	for _, ct := range it.trees {
+		if ct.canSyncRandom() {
+			it.syncableList = append(it.syncableList, ct)
+		} else {
+			it.disabledList = append(it.disabledList, ct)
+		}
+	}
+	if len(it.syncableList) > 0 {
+		return true, it.syncableList
+	}
+	return false, it.disabledList
+}
+
+// waitForRootUpdates waits for the closest scheduled root check time on the given trees.
+func (it *randomIterator) waitForRootUpdates(trees []*clientTree) bool {
+	var minTree *clientTree
+	var nextCheck mclock.AbsTime
+	for _, ct := range trees {
+		check := ct.nextScheduledRootCheck()
+		if minTree == nil || check < nextCheck {
+			minTree = ct
+			nextCheck = check
+		}
+	}
+
+	sleep := nextCheck.Sub(it.c.clock.Now())
+	it.c.cfg.Logger.Debug("DNS iterator waiting for root updates", "sleep", sleep, "tree", minTree.loc.domain)
+	timeout := it.c.clock.NewTimer(sleep)
+	defer timeout.Stop()
+	select {
+	case <-timeout.C():
+		return true
+	case <-it.ctx.Done():
+		return false // Iterator was closed.
+	}
+}
+
+// rebuildTrees rebuilds the 'trees' map.
+func (it *randomIterator) rebuildTrees() {
+	// Delete removed trees.
+	for loc := range it.trees {
+		if !it.lc.isReferenced(loc) {
+			delete(it.trees, loc)
+		}
+	}
+	// Add new trees.
+	for loc := range it.lc.backrefs {
+		if it.trees[loc] == nil {
+			link, _ := parseLink(linkPrefix + loc)
+			it.trees[loc] = newClientTree(it.c, &it.lc, link)
+		}
+	}
+}

+ 18 - 0
p2p/dnsdisc/doc.go

@@ -0,0 +1,18 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+// Package dnsdisc implements node discovery via DNS (EIP-1459).
+package dnsdisc

+ 63 - 0
p2p/dnsdisc/error.go

@@ -0,0 +1,63 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <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)
+}

+ 329 - 0
p2p/dnsdisc/sync.go

@@ -0,0 +1,329 @@
+// 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"
+	"math/rand"
+	"time"
+
+	"blockchain-go/common/mclock"
+	"blockchain-go/p2p/enode"
+)
+
+// This is the number of consecutive leaf requests that may fail before
+// we consider re-resolving the tree root.
+const rootRecheckFailCount = 5
+
+// clientTree is a full tree being synced.
+type clientTree struct {
+	c   *Client
+	loc *linkEntry // link to this tree
+
+	lastRootCheck mclock.AbsTime // last revalidation of root
+	leafFailCount int
+	rootFailCount int
+
+	root  *rootEntry
+	enrs  *subtreeSync
+	links *subtreeSync
+
+	lc         *linkCache          // tracks all links between all trees
+	curLinks   map[string]struct{} // links contained in this tree
+	linkGCRoot string              // root on which last link GC has run
+}
+
+func newClientTree(c *Client, lc *linkCache, loc *linkEntry) *clientTree {
+	return &clientTree{c: c, lc: lc, loc: loc}
+}
+
+// syncAll retrieves all entries of the tree.
+func (ct *clientTree) syncAll(dest map[string]entry) error {
+	if err := ct.updateRoot(context.Background()); 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) (n *enode.Node, err error) {
+	if ct.rootUpdateDue() {
+		if err := ct.updateRoot(ctx); err != nil {
+			return nil, err
+		}
+	}
+
+	// Update fail counter for leaf request errors.
+	defer func() {
+		if err != nil {
+			ct.leafFailCount++
+		}
+	}()
+
+	// Link tree sync has priority, run it to completion before syncing ENRs.
+	if !ct.links.done() {
+		err := ct.syncNextLink(ctx)
+		return nil, err
+	}
+	ct.gcLinks()
+
+	// Sync next random entry in ENR tree. Once every node has been visited, we simply
+	// start over. This is fine because entries are cached internally by the client LRU
+	// also by DNS resolvers.
+	if ct.enrs.done() {
+		ct.enrs = newSubtreeSync(ct.c, ct.loc, ct.root.eroot, false)
+	}
+	return ct.syncNextRandomENR(ctx)
+}
+
+// canSyncRandom checks if any meaningful action can be performed by syncRandom.
+func (ct *clientTree) canSyncRandom() bool {
+	// Note: the check for non-zero leaf count is very important here.
+	// If we're done syncing all nodes, and no leaves were found, the tree
+	// is empty and we can't use it for sync.
+	return ct.rootUpdateDue() || !ct.links.done() || !ct.enrs.done() || ct.enrs.leaves != 0
+}
+
+// gcLinks removes outdated links from the global link cache. GC runs once
+// when the link sync finishes.
+func (ct *clientTree) gcLinks() {
+	if !ct.links.done() || ct.root.lroot == ct.linkGCRoot {
+		return
+	}
+	ct.lc.resetLinks(ct.loc.str, ct.curLinks)
+	ct.linkGCRoot = ct.root.lroot
+}
+
+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 dest, ok := e.(*linkEntry); ok {
+		ct.lc.addLink(ct.loc.str, dest.str)
+		ct.curLinks[dest.str] = struct{}{}
+	}
+	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.String()
+}
+
+// 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(ctx context.Context) error {
+	if !ct.slowdownRootUpdate(ctx) {
+		return ctx.Err()
+	}
+
+	ct.lastRootCheck = ct.c.clock.Now()
+	ctx, cancel := context.WithTimeout(ctx, ct.c.cfg.Timeout)
+	defer cancel()
+	root, err := ct.c.resolveRoot(ctx, ct.loc)
+	if err != nil {
+		ct.rootFailCount++
+		return err
+	}
+	ct.root = &root
+	ct.rootFailCount = 0
+	ct.leafFailCount = 0
+
+	// Invalidate subtrees if changed.
+	if ct.links == nil || root.lroot != ct.links.root {
+		ct.links = newSubtreeSync(ct.c, ct.loc, root.lroot, true)
+		ct.curLinks = make(map[string]struct{})
+	}
+	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 {
+	tooManyFailures := ct.leafFailCount > rootRecheckFailCount
+	scheduledCheck := ct.c.clock.Now() >= ct.nextScheduledRootCheck()
+	return ct.root == nil || tooManyFailures || scheduledCheck
+}
+
+func (ct *clientTree) nextScheduledRootCheck() mclock.AbsTime {
+	return ct.lastRootCheck.Add(ct.c.cfg.RecheckInterval)
+}
+
+// slowdownRootUpdate applies a delay to root resolution if is tried
+// too frequently. This avoids busy polling when the client is offline.
+// Returns true if the timeout passed, false if sync was canceled.
+func (ct *clientTree) slowdownRootUpdate(ctx context.Context) bool {
+	var delay time.Duration
+	switch {
+	case ct.rootFailCount > 20:
+		delay = 10 * time.Second
+	case ct.rootFailCount > 5:
+		delay = 5 * time.Second
+	default:
+		return true
+	}
+	timeout := ct.c.clock.NewTimer(delay)
+	defer timeout.Stop()
+	select {
+	case <-timeout.C():
+		return true
+	case <-ctx.Done():
+		return false
+	}
+}
+
+// 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
+	leaves  int      // counter of synced leaves
+}
+
+func newSubtreeSync(c *Client, loc *linkEntry, root string, link bool) *subtreeSync {
+	return &subtreeSync{c, loc, root, []string{root}, link, 0}
+}
+
+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
+		}
+		ts.leaves++
+	case *linkEntry:
+		if !ts.link {
+			return nil, errLinkInENRTree
+		}
+		ts.leaves++
+	case *branchEntry:
+		ts.missing = append(ts.missing, e.children...)
+	}
+	return e, nil
+}
+
+// linkCache tracks links between trees.
+type linkCache struct {
+	backrefs map[string]map[string]struct{}
+	changed  bool
+}
+
+func (lc *linkCache) isReferenced(r string) bool {
+	return len(lc.backrefs[r]) != 0
+}
+
+func (lc *linkCache) addLink(from, to string) {
+	if _, ok := lc.backrefs[to][from]; ok {
+		return
+	}
+
+	if lc.backrefs == nil {
+		lc.backrefs = make(map[string]map[string]struct{})
+	}
+	if _, ok := lc.backrefs[to]; !ok {
+		lc.backrefs[to] = make(map[string]struct{})
+	}
+	lc.backrefs[to][from] = struct{}{}
+	lc.changed = true
+}
+
+// resetLinks clears all links of the given tree.
+func (lc *linkCache) resetLinks(from string, keep map[string]struct{}) {
+	stk := []string{from}
+	for len(stk) > 0 {
+		item := stk[len(stk)-1]
+		stk = stk[:len(stk)-1]
+
+		for r, refs := range lc.backrefs {
+			if _, ok := keep[r]; ok {
+				continue
+			}
+			if _, ok := refs[item]; !ok {
+				continue
+			}
+			lc.changed = true
+			delete(refs, item)
+			if len(refs) == 0 {
+				delete(lc.backrefs, r)
+				stk = append(stk, r)
+			}
+		}
+	}
+}

+ 423 - 0
p2p/dnsdisc/tree.go

@@ -0,0 +1,423 @@
+// 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"
+
+	"blockchain-go/p2p/enode"
+	"blockchain-go/p2p/enr"
+	"blockchain-go/rlp"
+	"github.com/ethereum/go-ethereum/crypto"
+	"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 := newLinkEntry(domain, &key.PublicKey)
+	return link.String(), 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.String())
+		}
+	}
+	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
+}
+
+/*
+We want to keep the UDP size below 512 bytes. The UDP size is roughly:
+UDP length = 8 + UDP payload length ( 229 )
+UPD Payload length:
+  - dns.id 2
+  - dns.flags 2
+  - dns.count.queries 2
+  - dns.count.answers 2
+  - dns.count.auth_rr 2
+  - dns.count.add_rr 2
+  - queries (query-size + 6)
+  - answers :
+  - dns.resp.name 2
+  - dns.resp.type 2
+  - dns.resp.class 2
+  - dns.resp.ttl 4
+  - dns.resp.len 2
+  - dns.txt.length 1
+  - dns.txt resp_data_size
+
+So the total size is roughly a fixed overhead of `39`, and the size of the
+query (domain name) and response.
+The query size is, for example, FVY6INQ6LZ33WLCHO3BPR3FH6Y.snap.mainnet.ethdisco.net (52)
+
+We also have some static data in the response, such as `enrtree-branch:`, and potentially
+splitting the response up with `" "`, leaving us with a size of roughly `400` that we need
+to stay below.
+
+The number `370` is used to have some margin for extra overhead (for example, the dns query
+may be larger - more subdomains).
+*/
+const (
+	hashAbbrevSize = 1 + 16*13/8          // Size of an encoded hash (plus comma)
+	maxChildren    = 370 / hashAbbrevSize // 13 children
+	minHashLength  = 12
+)
+
+// 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 := parseLink(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 &branchEntry{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
+	}
+	branchEntry struct {
+		children []string
+	}
+	enrEntry struct {
+		node *enode.Node
+	}
+	linkEntry struct {
+		str    string
+		domain string
+		pubkey *ecdsa.PublicKey
+	}
+)
+
+// Entry Encoding
+
+var (
+	b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
+	b64format = base64.RawURLEncoding
+)
+
+const (
+	rootPrefix   = "enrtree-root:v1"
+	linkPrefix   = "enrtree://"
+	branchPrefix = "enrtree-branch:"
+	enrPrefix    = "enr:"
+)
+
+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
+	enckey := crypto.FromECDSAPub(pubkey)
+	return crypto.VerifySignature(enckey, e.sigHash(), sig)
+}
+
+func (e *branchEntry) String() string {
+	return branchPrefix + strings.Join(e.children, ",")
+}
+
+func (e *enrEntry) String() string {
+	return e.node.String()
+}
+
+func (e *linkEntry) String() string {
+	return linkPrefix + e.str
+}
+
+func newLinkEntry(domain string, pubkey *ecdsa.PublicKey) *linkEntry {
+	key := b32format.EncodeToString(crypto.CompressPubkey(pubkey))
+	str := key + "@" + domain
+	return &linkEntry{str, domain, pubkey}
+}
+
+// Entry Parsing
+
+func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
+	switch {
+	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
+	}
+}
+
+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 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}
+	}
+	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{e, domain, key}, nil
+}
+
+func parseBranch(e string) (entry, error) {
+	e = e[len(branchPrefix):]
+	if e == "" {
+		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{"branch", errInvalidChild}
+		}
+		hashes = append(hashes, c)
+	}
+	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}
+	}
+	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 := parseLink(url)
+	if err != nil {
+		return "", nil, err
+	}
+	return le.domain, le.pubkey, nil
+}

+ 4 - 7
p2p/server.go

@@ -3,6 +3,7 @@ package p2p
 import (
 	"blockchain-go/common/gopool"
 	"blockchain-go/p2p/discover"
+	"blockchain-go/p2p/dnsdisc"
 	"blockchain-go/p2p/enode"
 	"blockchain-go/p2p/enr"
 	"blockchain-go/p2p/nat"
@@ -163,13 +164,9 @@ func (server *Server) setupDiscovery() (err error) {
 	server.discmix = enode.NewFairMix(discmixTimeout)
 
 	// 添加特定协议的发现源。
-	added := make(map[string]bool)
-	for _, proto := range server.Protocols {
-		if proto.DialCandidates != nil && !added[proto.Name] {
-			server.discmix.AddSource(proto.DialCandidates)
-			added[proto.Name] = true
-		}
-	}
+	dnsclient := dnsdisc.NewClient(dnsdisc.Config{})
+	dialCandidates, err := dnsclient.NewIterator()
+	server.discmix.AddSource(dialCandidates)
 
 	addr, err := net.ResolveUDPAddr("udp", server.ListenAddr)
 	if err != nil {