From bdb049e21521fa12841b19a2380065500ac7b785 Mon Sep 17 00:00:00 2001 From: George Tankersley Date: Wed, 9 Oct 2019 18:32:15 -0400 Subject: [PATCH] initial commit --- README.md | 5 ++ go.mod | 8 ++ go.sum | 36 +++++++++ zcash/client.go | 148 ++++++++++++++++++++++++++++++++++++ zcash/client_test.go | 44 +++++++++++ zcash/network/magic.go | 58 ++++++++++++++ zcash/network/magic_test.go | 27 +++++++ zcash/network/params.go | 47 ++++++++++++ zcash/storage.go | 3 + 9 files changed, 376 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 zcash/client.go create mode 100644 zcash/client_test.go create mode 100644 zcash/network/magic.go create mode 100644 zcash/network/magic_test.go create mode 100644 zcash/network/params.go create mode 100644 zcash/storage.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dcff14 --- /dev/null +++ b/README.md @@ -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. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e2dcb33 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d2769f3 --- /dev/null +++ b/go.sum @@ -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= diff --git a/zcash/client.go b/zcash/client.go new file mode 100644 index 0000000..ce717b1 --- /dev/null +++ b/zcash/client.go @@ -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() +} diff --git a/zcash/client_test.go b/zcash/client_test.go new file mode 100644 index 0000000..b7cf719 --- /dev/null +++ b/zcash/client_test.go @@ -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") + } +} diff --git a/zcash/network/magic.go b/zcash/network/magic.go new file mode 100644 index 0000000..92c132c --- /dev/null +++ b/zcash/network/magic.go @@ -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 + } +} diff --git a/zcash/network/magic_test.go b/zcash/network/magic_test.go new file mode 100644 index 0000000..f80977e --- /dev/null +++ b/zcash/network/magic_test.go @@ -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") + } +} diff --git a/zcash/network/params.go b/zcash/network/params.go new file mode 100644 index 0000000..d5997bd --- /dev/null +++ b/zcash/network/params.go @@ -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 +} diff --git a/zcash/storage.go b/zcash/storage.go new file mode 100644 index 0000000..fa75050 --- /dev/null +++ b/zcash/storage.go @@ -0,0 +1,3 @@ +package zcash + +// sqlite storage: resultFn writes to resp buffer