initial commit
This commit is contained in:
commit
bdb049e215
|
@ -0,0 +1,5 @@
|
||||||
|
## Zcash Network Crawler
|
||||||
|
|
||||||
|
This is a CoreDNS plugin that scrapes addresses of peers from a Zcash network. It's intended as a safer and more scalable replacement for the [zcash-seeder](https://github.com/zcash/zcash-seeder) project.
|
||||||
|
|
||||||
|
It uses [btcsuite](https://github.com/btcsuite) for networking and [crawshaw/sqlite](https://crawshaw.io/blog/go-and-sqlite) for storing peer data.
|
|
@ -0,0 +1,8 @@
|
||||||
|
module github.com/gtank/coredns-zcash
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/btcsuite/btcd v0.0.0-20190926002857-ba530c4abb35
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
)
|
|
@ -0,0 +1,36 @@
|
||||||
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
|
github.com/btcsuite/btcd v0.0.0-20190926002857-ba530c4abb35 h1:o2mPiVrkVzpBg/Q+lSfuf/92pEgsSIJvsQ13DyHs/3A=
|
||||||
|
github.com/btcsuite/btcd v0.0.0-20190926002857-ba530c4abb35/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
|
||||||
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
||||||
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
|
||||||
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
|
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||||
|
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY=
|
||||||
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,148 @@
|
||||||
|
package zcash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/peer"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
|
||||||
|
"github.com/gtank/coredns-zcash/zcash/network"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultPeerConfig = &peer.Config{
|
||||||
|
UserAgentName: "MagicBean",
|
||||||
|
UserAgentVersion: "2.0.7",
|
||||||
|
ChainParams: nil,
|
||||||
|
Services: 0,
|
||||||
|
TrickleInterval: time.Second * 10,
|
||||||
|
ProtocolVersion: 170009, // Blossom
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = log.New(os.Stdout, "zcash_client: ", log.Ldate|log.Ltime|log.Lshortfile|log.LUTC)
|
||||||
|
|
||||||
|
type Seeder struct {
|
||||||
|
peer *peer.Peer
|
||||||
|
config *peer.Config
|
||||||
|
|
||||||
|
handshakeComplete chan *peer.Peer
|
||||||
|
handshakePendingPeers map[string]*peer.Peer
|
||||||
|
livePeers map[string]*peer.Peer
|
||||||
|
|
||||||
|
// For mutating the above
|
||||||
|
peerState sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSeederPeerConfig(magic network.Network, template *peer.Config) (*peer.Config, error) {
|
||||||
|
var newPeerConfig peer.Config
|
||||||
|
|
||||||
|
// Load the default values
|
||||||
|
if template != nil {
|
||||||
|
newPeerConfig = *template
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := network.GetNetworkParams(magic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't construct peer config")
|
||||||
|
}
|
||||||
|
newPeerConfig.ChainParams = params
|
||||||
|
|
||||||
|
return &newPeerConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Seeder) OnVerAck(p *peer.Peer, msg *wire.MsgVerAck) {
|
||||||
|
s.peerState.RLock()
|
||||||
|
if s.handshakePendingPeers[p.Addr()] == nil {
|
||||||
|
logger.Printf("Got verack from unexpected peer %s", p.Addr())
|
||||||
|
s.peerState.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.peerState.RUnlock()
|
||||||
|
|
||||||
|
logger.Printf("Handshake completed with new peer %s", p.Addr())
|
||||||
|
|
||||||
|
s.peerState.Lock()
|
||||||
|
delete(s.handshakePendingPeers, p.Addr())
|
||||||
|
s.livePeers[p.Addr()] = p
|
||||||
|
s.peerState.Unlock()
|
||||||
|
|
||||||
|
s.handshakeComplete <- p
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeeder(network network.Network) (*Seeder, error) {
|
||||||
|
config, err := newSeederPeerConfig(network, defaultPeerConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not construct seeder")
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeeder := Seeder{
|
||||||
|
config: config,
|
||||||
|
handshakeComplete: make(chan *peer.Peer, 1),
|
||||||
|
handshakePendingPeers: make(map[string]*peer.Peer),
|
||||||
|
livePeers: make(map[string]*peer.Peer),
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeeder.config.Listeners.OnVerAck = newSeeder.OnVerAck
|
||||||
|
|
||||||
|
return &newSeeder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectToPeer attempts to connect to a peer on the default port at the
|
||||||
|
// specified address. It returns either a live peer connection or an error.
|
||||||
|
func (s *Seeder) ConnectToPeer(addr string) (*peer.Peer, error) {
|
||||||
|
connectionString := net.JoinHostPort(addr, s.config.ChainParams.DefaultPort)
|
||||||
|
|
||||||
|
p, err := peer.NewOutboundPeer(s.config, connectionString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "constructing outbound peer")
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", p.Addr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "dialing new peer address")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.peerState.Lock()
|
||||||
|
s.handshakePendingPeers[p.Addr()] = p
|
||||||
|
s.peerState.Unlock()
|
||||||
|
|
||||||
|
p.AssociateConnection(conn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case verackPeer := <-s.handshakeComplete:
|
||||||
|
if verackPeer.Addr() == p.Addr() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
return nil, errors.New("peer handshake timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("This should be unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Seeder) GracefulDisconnect() {
|
||||||
|
s.peerState.Lock()
|
||||||
|
|
||||||
|
for _, v := range s.handshakePendingPeers {
|
||||||
|
logger.Printf("Disconnecting from peer %s", v.Addr())
|
||||||
|
v.Disconnect()
|
||||||
|
v.WaitForDisconnect()
|
||||||
|
}
|
||||||
|
s.handshakePendingPeers = make(map[string]*peer.Peer)
|
||||||
|
|
||||||
|
for _, v := range s.livePeers {
|
||||||
|
logger.Printf("Disconnecting from peer %s", v.Addr())
|
||||||
|
v.Disconnect()
|
||||||
|
v.WaitForDisconnect()
|
||||||
|
}
|
||||||
|
s.livePeers = make(map[string]*peer.Peer)
|
||||||
|
|
||||||
|
s.peerState.Unlock()
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package zcash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gtank/coredns-zcash/zcash/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOutboundPeer(t *testing.T) {
|
||||||
|
regSeeder, err := NewSeeder(network.Regtest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = regSeeder.ConnectToPeer("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
regSeeder.GracefulDisconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutboundPeerAsync(t *testing.T) {
|
||||||
|
regSeeder, err := NewSeeder(network.Regtest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_, err := regSeeder.ConnectToPeer("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
regSeeder.GracefulDisconnect()
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
t.Error("timed out")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Network represents the byte sequences used to identify different Zcash networks.
|
||||||
|
type Network uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Mainnet identifies the Zcash mainnet
|
||||||
|
Mainnet Network = 0x6427e924
|
||||||
|
// Testnet identifies ECC's public testnet
|
||||||
|
Testnet Network = 0xbff91afa
|
||||||
|
// Regtest identifies the regression test network
|
||||||
|
Regtest Network = 0x5f3fe8aa
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidMagic = errors.New("invalid network magic")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal appends the 4-byte, little endian encoding of a network identifier
|
||||||
|
// to the dst slice and returns the resulting slice. If there is sufficient room
|
||||||
|
// in the dst slice, Marshal does not allocate.
|
||||||
|
func (m Network) Marshal(dst []byte) (out []byte) {
|
||||||
|
if n := len(dst) + 4; cap(dst) >= n {
|
||||||
|
out = dst[:n]
|
||||||
|
} else {
|
||||||
|
out = make([]byte, n)
|
||||||
|
copy(out, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint32(out[len(dst):], uint32(m))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parses a valid network identifier from a byte slice. It
|
||||||
|
// returns the identifier on success, zero and an error on failure.
|
||||||
|
func Decode(data []byte) (Network, error) {
|
||||||
|
if len(data) != 4 {
|
||||||
|
return 0, ErrInvalidMagic
|
||||||
|
}
|
||||||
|
|
||||||
|
number := Network(binary.LittleEndian.Uint32(data))
|
||||||
|
|
||||||
|
switch number {
|
||||||
|
case Mainnet:
|
||||||
|
return Mainnet, nil
|
||||||
|
case Testnet:
|
||||||
|
return Testnet, nil
|
||||||
|
case Regtest:
|
||||||
|
return Regtest, nil
|
||||||
|
default:
|
||||||
|
return 0, ErrInvalidMagic
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMainnetMagic(t *testing.T) {
|
||||||
|
// Zcash mainnet, src/chainparams.cpp
|
||||||
|
var pchMessageStart [4]byte
|
||||||
|
pchMessageStart[0] = 0x24
|
||||||
|
pchMessageStart[1] = 0xe9
|
||||||
|
pchMessageStart[2] = 0x27
|
||||||
|
pchMessageStart[3] = 0x64
|
||||||
|
|
||||||
|
magicBytes := Mainnet.Marshal(nil)
|
||||||
|
|
||||||
|
if !bytes.Equal(magicBytes, pchMessageStart[:]) {
|
||||||
|
t.Error("encoding failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
magic, err := Decode(pchMessageStart[:])
|
||||||
|
|
||||||
|
if err != nil || magic != Mainnet {
|
||||||
|
t.Error("decoding failed")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// These are not fully valid chainparams, but they'll do for a seeder.
|
||||||
|
regtestParams = chaincfg.Params{
|
||||||
|
Name: "regtest",
|
||||||
|
Net: wire.BitcoinNet(Regtest),
|
||||||
|
DefaultPort: "18344",
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are not fully valid chainparams, but they'll do for a seeder.
|
||||||
|
mainnetParams = chaincfg.Params{
|
||||||
|
Name: "mainnet",
|
||||||
|
Net: wire.BitcoinNet(Mainnet),
|
||||||
|
DefaultPort: "8233",
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are not fully valid chainparams, but they'll do for a seeder.
|
||||||
|
testnetParams = chaincfg.Params{
|
||||||
|
Name: "testnet",
|
||||||
|
Net: wire.BitcoinNet(Testnet),
|
||||||
|
DefaultPort: "18233",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetNetworkParams(magic Network) (*chaincfg.Params, error) {
|
||||||
|
var cfg chaincfg.Params
|
||||||
|
|
||||||
|
switch magic {
|
||||||
|
case Regtest:
|
||||||
|
cfg = regtestParams
|
||||||
|
case Mainnet:
|
||||||
|
cfg = mainnetParams
|
||||||
|
case Testnet:
|
||||||
|
cfg = testnetParams
|
||||||
|
default:
|
||||||
|
return nil, errors.Wrap(ErrInvalidMagic, "no network params")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package zcash
|
||||||
|
|
||||||
|
// sqlite storage: resultFn writes to resp buffer
|
Loading…
Reference in New Issue