chain: request pruned blocks from backend peers
At the moment, this is only done for the BitcoindClient, as the other backends don't support block pruning.
This commit is contained in:
parent
3fed46822c
commit
20c02df1e3
2 changed files with 136 additions and 19 deletions
|
@ -140,7 +140,7 @@ func (c *BitcoindClient) GetBlockHeight(hash *chainhash.Hash) (int32, error) {
|
||||||
|
|
||||||
// GetBlock returns a block from the hash.
|
// GetBlock returns a block from the hash.
|
||||||
func (c *BitcoindClient) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) {
|
func (c *BitcoindClient) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) {
|
||||||
return c.chainConn.client.GetBlock(hash)
|
return c.chainConn.GetBlock(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockVerbose returns a verbose block from the hash.
|
// GetBlockVerbose returns a verbose block from the hash.
|
||||||
|
|
|
@ -9,11 +9,13 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcjson"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/rpcclient"
|
"github.com/btcsuite/btcd/rpcclient"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightninglabs/gozmq"
|
"github.com/lightninglabs/gozmq"
|
||||||
|
"github.com/lightningnetwork/lnd/ticker"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -36,6 +38,10 @@ const (
|
||||||
// seqNumLen is the length of the sequence number of a message sent from
|
// seqNumLen is the length of the sequence number of a message sent from
|
||||||
// bitcoind through ZMQ.
|
// bitcoind through ZMQ.
|
||||||
seqNumLen = 4
|
seqNumLen = 4
|
||||||
|
|
||||||
|
// errBlockPrunedStr is the error message returned by bitcoind upon
|
||||||
|
// calling GetBlock on a pruned block.
|
||||||
|
errBlockPrunedStr = "Block not available (pruned data)"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BitcoindConfig contains all of the parameters required to establish a
|
// BitcoindConfig contains all of the parameters required to establish a
|
||||||
|
@ -66,6 +72,17 @@ type BitcoindConfig struct {
|
||||||
// ZMQReadDeadline represents the read deadline we'll apply when reading
|
// ZMQReadDeadline represents the read deadline we'll apply when reading
|
||||||
// ZMQ messages from either subscription.
|
// ZMQ messages from either subscription.
|
||||||
ZMQReadDeadline time.Duration
|
ZMQReadDeadline time.Duration
|
||||||
|
|
||||||
|
// Dialer is a closure we'll use to dial Bitcoin peers. If the chain
|
||||||
|
// backend is running over Tor, this must support dialing peers over Tor
|
||||||
|
// as well.
|
||||||
|
Dialer Dialer
|
||||||
|
|
||||||
|
// PrunedModeMaxPeers is the maximum number of peers we'll attempt to
|
||||||
|
// retrieve pruned blocks from.
|
||||||
|
//
|
||||||
|
// NOTE: This only applies for pruned bitcoind nodes.
|
||||||
|
PrunedModeMaxPeers int
|
||||||
}
|
}
|
||||||
|
|
||||||
// BitcoindConn represents a persistent client connection to a bitcoind node
|
// BitcoindConn represents a persistent client connection to a bitcoind node
|
||||||
|
@ -84,6 +101,11 @@ type BitcoindConn struct {
|
||||||
// client is the RPC client to the bitcoind node.
|
// client is the RPC client to the bitcoind node.
|
||||||
client *rpcclient.Client
|
client *rpcclient.Client
|
||||||
|
|
||||||
|
// prunedBlockDispatcher handles all of the pruned block requests.
|
||||||
|
//
|
||||||
|
// NOTE: This is nil when the bitcoind node is not pruned.
|
||||||
|
prunedBlockDispatcher *PrunedBlockDispatcher
|
||||||
|
|
||||||
// zmqBlockConn is the ZMQ connection we'll use to read raw block
|
// zmqBlockConn is the ZMQ connection we'll use to read raw block
|
||||||
// events.
|
// events.
|
||||||
zmqBlockConn *gozmq.Conn
|
zmqBlockConn *gozmq.Conn
|
||||||
|
@ -101,6 +123,10 @@ type BitcoindConn struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dialer represents a way to dial Bitcoin peers. If the chain backend is
|
||||||
|
// running over Tor, this must support dialing peers over Tor as well.
|
||||||
|
type Dialer = func(string) (net.Conn, error)
|
||||||
|
|
||||||
// NewBitcoindConn creates a client connection to the node described by the host
|
// NewBitcoindConn creates a client connection to the node described by the host
|
||||||
// string. The ZMQ connections are established immediately to ensure liveness.
|
// string. The ZMQ connections are established immediately to ensure liveness.
|
||||||
// If the remote node does not operate on the same bitcoin network as described
|
// If the remote node does not operate on the same bitcoin network as described
|
||||||
|
@ -120,6 +146,24 @@ func NewBitcoindConn(cfg *BitcoindConfig) (*BitcoindConn, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that the node is running on the expected network.
|
||||||
|
net, err := getCurrentNet(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if net != cfg.ChainParams.Net {
|
||||||
|
return nil, fmt.Errorf("expected network %v, got %v",
|
||||||
|
cfg.ChainParams.Net, net)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the node is pruned, as we'll need to perform additional
|
||||||
|
// operations if so.
|
||||||
|
chainInfo, err := client.GetBlockChainInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to determine if bitcoind is "+
|
||||||
|
"pruned: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Establish two different ZMQ connections to bitcoind to retrieve block
|
// Establish two different ZMQ connections to bitcoind to retrieve block
|
||||||
// and transaction event notifications. We'll use two as a separation of
|
// and transaction event notifications. We'll use two as a separation of
|
||||||
// concern to ensure one type of event isn't dropped from the connection
|
// concern to ensure one type of event isn't dropped from the connection
|
||||||
|
@ -142,16 +186,37 @@ func NewBitcoindConn(cfg *BitcoindConfig) (*BitcoindConn, error) {
|
||||||
"events: %v", err)
|
"events: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := &BitcoindConn{
|
// Only initialize the PrunedBlockDispatcher when the connected bitcoind
|
||||||
|
// node is pruned.
|
||||||
|
var prunedBlockDispatcher *PrunedBlockDispatcher
|
||||||
|
if chainInfo.Pruned {
|
||||||
|
prunedBlockDispatcher, err = NewPrunedBlockDispatcher(
|
||||||
|
&PrunedBlockDispatcherConfig{
|
||||||
|
ChainParams: cfg.ChainParams,
|
||||||
|
NumTargetPeers: cfg.PrunedModeMaxPeers,
|
||||||
|
Dial: cfg.Dialer,
|
||||||
|
GetPeers: client.GetPeerInfo,
|
||||||
|
PeerReadyTimeout: defaultPeerReadyTimeout,
|
||||||
|
RefreshPeersTicker: ticker.New(
|
||||||
|
defaultRefreshPeersInterval,
|
||||||
|
),
|
||||||
|
MaxRequestInvs: wire.MaxInvPerMsg,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BitcoindConn{
|
||||||
cfg: *cfg,
|
cfg: *cfg,
|
||||||
client: client,
|
client: client,
|
||||||
|
prunedBlockDispatcher: prunedBlockDispatcher,
|
||||||
zmqBlockConn: zmqBlockConn,
|
zmqBlockConn: zmqBlockConn,
|
||||||
zmqTxConn: zmqTxConn,
|
zmqTxConn: zmqTxConn,
|
||||||
rescanClients: make(map[uint64]*BitcoindClient),
|
rescanClients: make(map[uint64]*BitcoindClient),
|
||||||
quit: make(chan struct{}),
|
quit: make(chan struct{}),
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start attempts to establish a RPC and ZMQ connection to a bitcoind node. If
|
// Start attempts to establish a RPC and ZMQ connection to a bitcoind node. If
|
||||||
|
@ -164,14 +229,13 @@ func (c *BitcoindConn) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the node is running on the expected network.
|
// If we're connected to a pruned backend, we'll need to also start our
|
||||||
net, err := c.getCurrentNet()
|
// pruned block dispatcher to handle pruned block requests.
|
||||||
if err != nil {
|
if c.prunedBlockDispatcher != nil {
|
||||||
|
log.Debug("Detected pruned bitcoind backend")
|
||||||
|
if err := c.prunedBlockDispatcher.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if net != c.cfg.ChainParams.Net {
|
|
||||||
return fmt.Errorf("expected network %v, got %v",
|
|
||||||
c.cfg.ChainParams.Net, net)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.wg.Add(2)
|
c.wg.Add(2)
|
||||||
|
@ -197,6 +261,10 @@ func (c *BitcoindConn) Stop() {
|
||||||
c.zmqBlockConn.Close()
|
c.zmqBlockConn.Close()
|
||||||
c.zmqTxConn.Close()
|
c.zmqTxConn.Close()
|
||||||
|
|
||||||
|
if c.prunedBlockDispatcher != nil {
|
||||||
|
c.prunedBlockDispatcher.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
c.client.WaitForShutdown()
|
c.client.WaitForShutdown()
|
||||||
c.wg.Wait()
|
c.wg.Wait()
|
||||||
}
|
}
|
||||||
|
@ -405,8 +473,8 @@ func (c *BitcoindConn) txEventHandler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCurrentNet returns the network on which the bitcoind node is running.
|
// getCurrentNet returns the network on which the bitcoind node is running.
|
||||||
func (c *BitcoindConn) getCurrentNet() (wire.BitcoinNet, error) {
|
func getCurrentNet(client *rpcclient.Client) (wire.BitcoinNet, error) {
|
||||||
hash, err := c.client.GetBlockHash(0)
|
hash, err := client.GetBlockHash(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -472,6 +540,55 @@ func (c *BitcoindConn) RemoveClient(id uint64) {
|
||||||
delete(c.rescanClients, id)
|
delete(c.rescanClients, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBlockPrunedErr determines if the error returned by the GetBlock RPC
|
||||||
|
// corresponds to the requested block being pruned.
|
||||||
|
func isBlockPrunedErr(err error) bool {
|
||||||
|
rpcErr, ok := err.(*btcjson.RPCError)
|
||||||
|
return ok && rpcErr.Code == btcjson.ErrRPCMisc &&
|
||||||
|
rpcErr.Message == errBlockPrunedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlock returns a raw block from the server given its hash. If the server
|
||||||
|
// has already pruned the block, it will be retrieved from one of its peers.
|
||||||
|
func (c *BitcoindConn) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) {
|
||||||
|
block, err := c.client.GetBlock(hash)
|
||||||
|
// Got the block from the backend successfully, return it.
|
||||||
|
if err == nil {
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We failed getting the block from the backend for whatever reason. If
|
||||||
|
// it wasn't due to the block being pruned, return the error
|
||||||
|
// immediately.
|
||||||
|
if !isBlockPrunedErr(err) || c.prunedBlockDispatcher == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we know the block has been pruned for sure, request it from
|
||||||
|
// our backend peers.
|
||||||
|
blockChan, errChan := c.prunedBlockDispatcher.Query(
|
||||||
|
[]*chainhash.Hash{hash},
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case block := <-blockChan:
|
||||||
|
return block, nil
|
||||||
|
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// errChan fired before blockChan with a nil error, wait
|
||||||
|
// for the block now.
|
||||||
|
|
||||||
|
case <-c.quit:
|
||||||
|
return nil, ErrBitcoindClientShuttingDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// isASCII is a helper method that checks whether all bytes in `data` would be
|
// isASCII is a helper method that checks whether all bytes in `data` would be
|
||||||
// printable ASCII characters if interpreted as a string.
|
// printable ASCII characters if interpreted as a string.
|
||||||
func isASCII(s string) bool {
|
func isASCII(s string) bool {
|
||||||
|
|
Loading…
Reference in a new issue