initial commit

This commit is contained in:
George Tankersley 2019-10-09 18:32:15 -04:00
commit bdb049e215
9 changed files with 376 additions and 0 deletions

5
README.md Normal file
View File

@ -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.

8
go.mod Normal file
View File

@ -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
)

36
go.sum Normal file
View File

@ -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=

148
zcash/client.go Normal file
View File

@ -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()
}

44
zcash/client_test.go Normal file
View File

@ -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")
}
}

58
zcash/network/magic.go Normal file
View File

@ -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
}
}

View File

@ -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")
}
}

47
zcash/network/params.go Normal file
View File

@ -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
}

3
zcash/storage.go Normal file
View File

@ -0,0 +1,3 @@
package zcash
// sqlite storage: resultFn writes to resp buffer