mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-29 19:22:40 +01:00
multi: add bitcoind drivers and tests
This commit is contained in:
parent
244ae4b571
commit
187f59556a
679
chainntnfs/bitcoindnotify/bitcoind.go
Normal file
679
chainntnfs/bitcoindnotify/bitcoind.go
Normal file
@ -0,0 +1,679 @@
|
|||||||
|
package bitcoindnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/roasbeef/btcd/btcjson"
|
||||||
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/roasbeef/btcd/rpcclient"
|
||||||
|
"github.com/roasbeef/btcd/wire"
|
||||||
|
"github.com/roasbeef/btcutil"
|
||||||
|
"github.com/roasbeef/btcwallet/chain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// notifierType uniquely identifies this concrete implementation of the
|
||||||
|
// ChainNotifier interface.
|
||||||
|
notifierType = "bitcoind"
|
||||||
|
|
||||||
|
// reorgSafetyLimit is assumed maximum depth of a chain reorganization.
|
||||||
|
// After this many confirmation, transaction confirmation info will be
|
||||||
|
// pruned.
|
||||||
|
reorgSafetyLimit = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrChainNotifierShuttingDown is used when we are trying to
|
||||||
|
// measure a spend notification when notifier is already stopped.
|
||||||
|
ErrChainNotifierShuttingDown = errors.New("chainntnfs: system interrupt " +
|
||||||
|
"while attempting to register for spend notification.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// chainUpdate encapsulates an update to the current main chain. This struct is
|
||||||
|
// used as an element within an unbounded queue in order to avoid blocking the
|
||||||
|
// main rpc dispatch rule.
|
||||||
|
type chainUpdate struct {
|
||||||
|
blockHash *chainhash.Hash
|
||||||
|
blockHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// txUpdate encapsulates a transaction related notification sent from bitcoind
|
||||||
|
// to the registered RPC client. This struct is used as an element within an
|
||||||
|
// unbounded queue in order to avoid blocking the main rpc dispatch rule.
|
||||||
|
type txUpdate struct {
|
||||||
|
tx *btcutil.Tx
|
||||||
|
details *btcjson.BlockDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(roasbeef): generalize struct below:
|
||||||
|
// * move chans to config, allow outside callers to handle send conditions
|
||||||
|
|
||||||
|
// BitcoindNotifier implements the ChainNotifier interface using a bitcoind
|
||||||
|
// chain client. Multiple concurrent clients are supported. All notifications
|
||||||
|
// are achieved via non-blocking sends on client channels.
|
||||||
|
type BitcoindNotifier struct {
|
||||||
|
spendClientCounter uint64 // To be used atomically.
|
||||||
|
epochClientCounter uint64 // To be used atomically.
|
||||||
|
|
||||||
|
started int32 // To be used atomically.
|
||||||
|
stopped int32 // To be used atomically.
|
||||||
|
|
||||||
|
heightMtx sync.RWMutex
|
||||||
|
bestHeight int32
|
||||||
|
|
||||||
|
chainConn *chain.BitcoindClient
|
||||||
|
|
||||||
|
notificationCancels chan interface{}
|
||||||
|
notificationRegistry chan interface{}
|
||||||
|
|
||||||
|
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
|
||||||
|
|
||||||
|
txConfNotifier *chainntnfs.TxConfNotifier
|
||||||
|
|
||||||
|
blockEpochClients map[uint64]*blockEpochRegistration
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure BitcoindNotifier implements the ChainNotifier interface at compile
|
||||||
|
// time.
|
||||||
|
var _ chainntnfs.ChainNotifier = (*BitcoindNotifier)(nil)
|
||||||
|
|
||||||
|
// New returns a new BitcoindNotifier instance. This function assumes the
|
||||||
|
// bitcoind node detailed in the passed configuration is already running, and
|
||||||
|
// willing to accept RPC requests and new zmq clients.
|
||||||
|
func New(config *rpcclient.ConnConfig, zmqConnect string,
|
||||||
|
params chaincfg.Params) (*BitcoindNotifier, error) {
|
||||||
|
notifier := &BitcoindNotifier{
|
||||||
|
notificationCancels: make(chan interface{}),
|
||||||
|
notificationRegistry: make(chan interface{}),
|
||||||
|
|
||||||
|
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
||||||
|
|
||||||
|
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
|
||||||
|
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable connecting to bitcoind within the rpcclient.New method. We
|
||||||
|
// defer establishing the connection to our .Start() method.
|
||||||
|
config.DisableConnectOnNew = true
|
||||||
|
config.DisableAutoReconnect = false
|
||||||
|
chainConn, err := chain.NewBitcoindClient(¶ms, config.Host,
|
||||||
|
config.User, config.Pass, zmqConnect, 100*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notifier.chainConn = chainConn
|
||||||
|
|
||||||
|
return notifier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start connects to the running bitcoind node over websockets, registers for
|
||||||
|
// block notifications, and finally launches all related helper goroutines.
|
||||||
|
func (b *BitcoindNotifier) Start() error {
|
||||||
|
// Already started?
|
||||||
|
if atomic.AddInt32(&b.started, 1) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to bitcoind, and register for notifications on connected,
|
||||||
|
// and disconnected blocks.
|
||||||
|
if err := b.chainConn.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := b.chainConn.NotifyBlocks(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, currentHeight, err := b.chainConn.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.heightMtx.Lock()
|
||||||
|
b.bestHeight = currentHeight
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
|
||||||
|
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
||||||
|
uint32(currentHeight), reorgSafetyLimit)
|
||||||
|
|
||||||
|
b.wg.Add(1)
|
||||||
|
go b.notificationDispatcher()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop shutsdown the BitcoindNotifier.
|
||||||
|
func (b *BitcoindNotifier) Stop() error {
|
||||||
|
// Already shutting down?
|
||||||
|
if atomic.AddInt32(&b.stopped, 1) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown the rpc client, this gracefully disconnects from bitcoind,
|
||||||
|
// and cleans up all related resources.
|
||||||
|
b.chainConn.Stop()
|
||||||
|
|
||||||
|
close(b.quit)
|
||||||
|
b.wg.Wait()
|
||||||
|
|
||||||
|
// Notify all pending clients of our shutdown by closing the related
|
||||||
|
// notification channels.
|
||||||
|
for _, spendClients := range b.spendNotifications {
|
||||||
|
for _, spendClient := range spendClients {
|
||||||
|
close(spendClient.spendChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, epochClient := range b.blockEpochClients {
|
||||||
|
close(epochClient.epochChan)
|
||||||
|
}
|
||||||
|
b.txConfNotifier.TearDown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockNtfn packages a notification of a connected/disconnected block along
|
||||||
|
// with its height at the time.
|
||||||
|
type blockNtfn struct {
|
||||||
|
sha *chainhash.Hash
|
||||||
|
height int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// notificationDispatcher is the primary goroutine which handles client
|
||||||
|
// notification registrations, as well as notification dispatches.
|
||||||
|
func (b *BitcoindNotifier) notificationDispatcher() {
|
||||||
|
out:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case cancelMsg := <-b.notificationCancels:
|
||||||
|
switch msg := cancelMsg.(type) {
|
||||||
|
case *spendCancel:
|
||||||
|
chainntnfs.Log.Infof("Cancelling spend "+
|
||||||
|
"notification for out_point=%v, "+
|
||||||
|
"spend_id=%v", msg.op, msg.spendID)
|
||||||
|
|
||||||
|
// Before we attempt to close the spendChan,
|
||||||
|
// ensure that the notification hasn't already
|
||||||
|
// yet been dispatched.
|
||||||
|
if outPointClients, ok := b.spendNotifications[msg.op]; ok {
|
||||||
|
close(outPointClients[msg.spendID].spendChan)
|
||||||
|
delete(b.spendNotifications[msg.op], msg.spendID)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *epochCancel:
|
||||||
|
chainntnfs.Log.Infof("Cancelling epoch "+
|
||||||
|
"notification, epoch_id=%v", msg.epochID)
|
||||||
|
|
||||||
|
// First, close the cancel channel for this
|
||||||
|
// specific client, and wait for the client to
|
||||||
|
// exit.
|
||||||
|
close(b.blockEpochClients[msg.epochID].cancelChan)
|
||||||
|
b.blockEpochClients[msg.epochID].wg.Wait()
|
||||||
|
|
||||||
|
// Once the client has exited, we can then
|
||||||
|
// safely close the channel used to send epoch
|
||||||
|
// notifications, in order to notify any
|
||||||
|
// listeners that the intent has been
|
||||||
|
// cancelled.
|
||||||
|
close(b.blockEpochClients[msg.epochID].epochChan)
|
||||||
|
delete(b.blockEpochClients, msg.epochID)
|
||||||
|
|
||||||
|
}
|
||||||
|
case registerMsg := <-b.notificationRegistry:
|
||||||
|
switch msg := registerMsg.(type) {
|
||||||
|
case *spendNotification:
|
||||||
|
chainntnfs.Log.Infof("New spend subscription: "+
|
||||||
|
"utxo=%v", msg.targetOutpoint)
|
||||||
|
op := *msg.targetOutpoint
|
||||||
|
|
||||||
|
if _, ok := b.spendNotifications[op]; !ok {
|
||||||
|
b.spendNotifications[op] = make(map[uint64]*spendNotification)
|
||||||
|
}
|
||||||
|
b.spendNotifications[op][msg.spendID] = msg
|
||||||
|
b.chainConn.NotifySpent([]*wire.OutPoint{&op})
|
||||||
|
case *confirmationsNotification:
|
||||||
|
chainntnfs.Log.Infof("New confirmations "+
|
||||||
|
"subscription: txid=%v, numconfs=%v",
|
||||||
|
msg.TxID, msg.NumConfirmations)
|
||||||
|
|
||||||
|
// Lookup whether the transaction is already included in the
|
||||||
|
// active chain.
|
||||||
|
txConf, err := b.historicalConfDetails(msg.TxID)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Error(err)
|
||||||
|
}
|
||||||
|
b.heightMtx.RLock()
|
||||||
|
err = b.txConfNotifier.Register(&msg.ConfNtfn, txConf)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Error(err)
|
||||||
|
}
|
||||||
|
b.heightMtx.RUnlock()
|
||||||
|
case *blockEpochRegistration:
|
||||||
|
chainntnfs.Log.Infof("New block epoch subscription")
|
||||||
|
b.blockEpochClients[msg.epochID] = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
case ntfn := <-b.chainConn.Notifications():
|
||||||
|
switch item := ntfn.(type) {
|
||||||
|
case chain.BlockConnected:
|
||||||
|
b.heightMtx.Lock()
|
||||||
|
if item.Height != b.bestHeight+1 {
|
||||||
|
chainntnfs.Log.Warnf("Received blocks out of order: "+
|
||||||
|
"current height=%d, new height=%d",
|
||||||
|
b.bestHeight, item.Height)
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.bestHeight = item.Height
|
||||||
|
|
||||||
|
rawBlock, err := b.chainConn.GetBlock(&item.Hash)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Unable to get block: %v", err)
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
chainntnfs.Log.Infof("New block: height=%v, sha=%v",
|
||||||
|
item.Height, item.Hash)
|
||||||
|
|
||||||
|
b.notifyBlockEpochs(item.Height, &item.Hash)
|
||||||
|
|
||||||
|
txns := btcutil.NewBlock(rawBlock).Transactions()
|
||||||
|
err = b.txConfNotifier.ConnectTip(&item.Hash,
|
||||||
|
uint32(item.Height), txns)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Error(err)
|
||||||
|
}
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
continue
|
||||||
|
|
||||||
|
case chain.BlockDisconnected:
|
||||||
|
b.heightMtx.Lock()
|
||||||
|
if item.Height != b.bestHeight {
|
||||||
|
chainntnfs.Log.Warnf("Received blocks "+
|
||||||
|
"out of order: current height="+
|
||||||
|
"%d, disconnected height=%d",
|
||||||
|
b.bestHeight, item.Height)
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.bestHeight = item.Height - 1
|
||||||
|
|
||||||
|
chainntnfs.Log.Infof("Block disconnected from "+
|
||||||
|
"main chain: height=%v, sha=%v",
|
||||||
|
item.Height, item.Hash)
|
||||||
|
|
||||||
|
err := b.txConfNotifier.DisconnectTip(
|
||||||
|
uint32(item.Height))
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Error(err)
|
||||||
|
}
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
|
||||||
|
case chain.RelevantTx:
|
||||||
|
tx := item.TxRecord.MsgTx
|
||||||
|
// First, check if this transaction spends an output
|
||||||
|
// that has an existing spend notification for it.
|
||||||
|
for i, txIn := range tx.TxIn {
|
||||||
|
prevOut := txIn.PreviousOutPoint
|
||||||
|
|
||||||
|
// If this transaction indeed does spend an
|
||||||
|
// output which we have a registered
|
||||||
|
// notification for, then create a spend
|
||||||
|
// summary, finally sending off the details to
|
||||||
|
// the notification subscriber.
|
||||||
|
if clients, ok := b.spendNotifications[prevOut]; ok {
|
||||||
|
spenderSha := tx.TxHash()
|
||||||
|
spendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &prevOut,
|
||||||
|
SpenderTxHash: &spenderSha,
|
||||||
|
SpendingTx: &tx,
|
||||||
|
SpenderInputIndex: uint32(i),
|
||||||
|
}
|
||||||
|
// TODO(roasbeef): after change to
|
||||||
|
// loadfilter, only notify on block
|
||||||
|
// inclusion?
|
||||||
|
if item.Block != nil {
|
||||||
|
spendDetails.SpendingHeight = item.Block.Height
|
||||||
|
} else {
|
||||||
|
b.heightMtx.RLock()
|
||||||
|
spendDetails.SpendingHeight = b.bestHeight + 1
|
||||||
|
b.heightMtx.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ntfn := range clients {
|
||||||
|
chainntnfs.Log.Infof("Dispatching "+
|
||||||
|
"spend notification for "+
|
||||||
|
"outpoint=%v", ntfn.targetOutpoint)
|
||||||
|
ntfn.spendChan <- spendDetails
|
||||||
|
|
||||||
|
// Close spendChan to ensure that any calls to Cancel will not
|
||||||
|
// block. This is safe to do since the channel is buffered, and the
|
||||||
|
// message can still be read by the receiver.
|
||||||
|
close(ntfn.spendChan)
|
||||||
|
}
|
||||||
|
delete(b.spendNotifications, prevOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-b.quit:
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// historicalConfDetails looks up whether a transaction is already included in a
|
||||||
|
// block in the active chain and, if so, returns details about the confirmation.
|
||||||
|
func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||||
|
) (*chainntnfs.TxConfirmation, error) {
|
||||||
|
|
||||||
|
// If the transaction already has some or all of the confirmations,
|
||||||
|
// then we may be able to dispatch it immediately.
|
||||||
|
// TODO: fall back to scanning blocks if txindex isn't on.
|
||||||
|
tx, err := b.chainConn.GetRawTransactionVerbose(txid)
|
||||||
|
if err != nil || tx == nil || tx.BlockHash == "" {
|
||||||
|
if err == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// Do not return an error if the transaction was not found.
|
||||||
|
if jsonErr, ok := err.(*btcjson.RPCError); ok {
|
||||||
|
if jsonErr.Code == btcjson.ErrRPCNoTxInfo {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to query for txid(%v): %v", txid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As we need to fully populate the returned TxConfirmation struct,
|
||||||
|
// grab the block in which the transaction was confirmed so we can
|
||||||
|
// locate its exact index within the block.
|
||||||
|
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get block hash %v for historical "+
|
||||||
|
"dispatch: %v", tx.BlockHash, err)
|
||||||
|
}
|
||||||
|
block, err := b.chainConn.GetBlockVerbose(blockHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get block hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the block obtained, locate the transaction's index within the
|
||||||
|
// block so we can give the subscriber full confirmation details.
|
||||||
|
txIndex := -1
|
||||||
|
targetTxidStr := txid.String()
|
||||||
|
for i, txHash := range block.Tx {
|
||||||
|
if txHash == targetTxidStr {
|
||||||
|
txIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if txIndex == -1 {
|
||||||
|
return nil, fmt.Errorf("unable to locate tx %v in block %v",
|
||||||
|
txid, blockHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
txConf := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: blockHash,
|
||||||
|
BlockHeight: uint32(block.Height),
|
||||||
|
TxIndex: uint32(txIndex),
|
||||||
|
}
|
||||||
|
return &txConf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyBlockEpochs notifies all registered block epoch clients of the newly
|
||||||
|
// connected block to the main chain.
|
||||||
|
func (b *BitcoindNotifier) notifyBlockEpochs(newHeight int32, newSha *chainhash.Hash) {
|
||||||
|
epoch := &chainntnfs.BlockEpoch{
|
||||||
|
Height: newHeight,
|
||||||
|
Hash: newSha,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, epochClient := range b.blockEpochClients {
|
||||||
|
b.wg.Add(1)
|
||||||
|
epochClient.wg.Add(1)
|
||||||
|
go func(ntfnChan chan *chainntnfs.BlockEpoch, cancelChan chan struct{},
|
||||||
|
clientWg *sync.WaitGroup) {
|
||||||
|
|
||||||
|
// TODO(roasbeef): move to goroutine per client, use sync queue
|
||||||
|
|
||||||
|
defer clientWg.Done()
|
||||||
|
defer b.wg.Done()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfnChan <- epoch:
|
||||||
|
|
||||||
|
case <-cancelChan:
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-b.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}(epochClient.epochChan, epochClient.cancelChan, &epochClient.wg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spendNotification couples a target outpoint along with the channel used for
|
||||||
|
// notifications once a spend of the outpoint has been detected.
|
||||||
|
type spendNotification struct {
|
||||||
|
targetOutpoint *wire.OutPoint
|
||||||
|
|
||||||
|
spendChan chan *chainntnfs.SpendDetail
|
||||||
|
|
||||||
|
spendID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// spendCancel is a message sent to the BitcoindNotifier when a client wishes
|
||||||
|
// to cancel an outstanding spend notification that has yet to be dispatched.
|
||||||
|
type spendCancel struct {
|
||||||
|
// op is the target outpoint of the notification to be cancelled.
|
||||||
|
op wire.OutPoint
|
||||||
|
|
||||||
|
// spendID the ID of the notification to cancel.
|
||||||
|
spendID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSpendNtfn registers an intent to be notified once the target
|
||||||
|
// outpoint has been spent by a transaction on-chain. Once a spend of the target
|
||||||
|
// outpoint has been detected, the details of the spending event will be sent
|
||||||
|
// across the 'Spend' channel.
|
||||||
|
func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
||||||
|
_ uint32) (*chainntnfs.SpendEvent, error) {
|
||||||
|
|
||||||
|
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn := &spendNotification{
|
||||||
|
targetOutpoint: outpoint,
|
||||||
|
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||||
|
spendID: atomic.AddUint64(&b.spendClientCounter, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-b.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
case b.notificationRegistry <- ntfn:
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following conditional checks to ensure that when a spend notification
|
||||||
|
// is registered, the output hasn't already been spent. If the output
|
||||||
|
// is no longer in the UTXO set, the chain will be rescanned from the point
|
||||||
|
// where the output was added. The rescan will dispatch the notification.
|
||||||
|
txout, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if txout == nil {
|
||||||
|
// TODO: fall back to scanning blocks if txindex isn't on.
|
||||||
|
transaction, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
||||||
|
if err != nil {
|
||||||
|
jsonErr, ok := err.(*btcjson.RPCError)
|
||||||
|
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if transaction != nil {
|
||||||
|
blockhash, err := chainhash.NewHashFromStr(transaction.BlockHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewind the rescan, since the btcwallet bitcoind
|
||||||
|
// back-end doesn't support that.
|
||||||
|
blockHeight, err := b.chainConn.GetBlockHeight(blockhash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.heightMtx.Lock()
|
||||||
|
currentHeight := b.bestHeight
|
||||||
|
b.bestHeight = blockHeight
|
||||||
|
for i := currentHeight; i > blockHeight; i-- {
|
||||||
|
err = b.txConfNotifier.DisconnectTip(uint32(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.heightMtx.Unlock()
|
||||||
|
|
||||||
|
ops := []*wire.OutPoint{outpoint}
|
||||||
|
if err := b.chainConn.Rescan(blockhash, nil, ops); err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Rescan for spend "+
|
||||||
|
"notification txout failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chainntnfs.SpendEvent{
|
||||||
|
Spend: ntfn.spendChan,
|
||||||
|
Cancel: func() {
|
||||||
|
cancel := &spendCancel{
|
||||||
|
op: *outpoint,
|
||||||
|
spendID: ntfn.spendID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit spend cancellation to notification dispatcher.
|
||||||
|
select {
|
||||||
|
case b.notificationCancels <- cancel:
|
||||||
|
// Cancellation is being handled, drain the spend chan until it is
|
||||||
|
// closed before yielding to the caller.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-ntfn.spendChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-b.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-b.quit:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirmationNotification represents a client's intent to receive a
|
||||||
|
// notification once the target txid reaches numConfirmations confirmations.
|
||||||
|
type confirmationsNotification struct {
|
||||||
|
chainntnfs.ConfNtfn
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterConfirmationsNtfn registers a notification with BitcoindNotifier
|
||||||
|
// which will be triggered once the txid reaches numConfs number of
|
||||||
|
// confirmations.
|
||||||
|
func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
||||||
|
numConfs, _ uint32) (*chainntnfs.ConfirmationEvent, error) {
|
||||||
|
|
||||||
|
ntfn := &confirmationsNotification{
|
||||||
|
chainntnfs.ConfNtfn{
|
||||||
|
TxID: txid,
|
||||||
|
NumConfirmations: numConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-b.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
case b.notificationRegistry <- ntfn:
|
||||||
|
return ntfn.Event, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockEpochRegistration represents a client's intent to receive a
|
||||||
|
// notification with each newly connected block.
|
||||||
|
type blockEpochRegistration struct {
|
||||||
|
epochID uint64
|
||||||
|
|
||||||
|
epochChan chan *chainntnfs.BlockEpoch
|
||||||
|
|
||||||
|
cancelChan chan struct{}
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// epochCancel is a message sent to the BitcoindNotifier when a client wishes
|
||||||
|
// to cancel an outstanding epoch notification that has yet to be dispatched.
|
||||||
|
type epochCancel struct {
|
||||||
|
epochID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the
|
||||||
|
// caller to receive notifications, of each new block connected to the main
|
||||||
|
// chain.
|
||||||
|
func (b *BitcoindNotifier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) {
|
||||||
|
registration := &blockEpochRegistration{
|
||||||
|
epochChan: make(chan *chainntnfs.BlockEpoch, 20),
|
||||||
|
cancelChan: make(chan struct{}),
|
||||||
|
epochID: atomic.AddUint64(&b.epochClientCounter, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-b.quit:
|
||||||
|
return nil, errors.New("chainntnfs: system interrupt while " +
|
||||||
|
"attempting to register for block epoch notification.")
|
||||||
|
case b.notificationRegistry <- registration:
|
||||||
|
return &chainntnfs.BlockEpochEvent{
|
||||||
|
Epochs: registration.epochChan,
|
||||||
|
Cancel: func() {
|
||||||
|
cancel := &epochCancel{
|
||||||
|
epochID: registration.epochID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit epoch cancellation to notification dispatcher.
|
||||||
|
select {
|
||||||
|
case b.notificationCancels <- cancel:
|
||||||
|
// Cancellation is being handled, drain the epoch channel until it is
|
||||||
|
// closed before yielding to caller.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-registration.epochChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-b.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-b.quit:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
53
chainntnfs/bitcoindnotify/driver.go
Normal file
53
chainntnfs/bitcoindnotify/driver.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package bitcoindnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
|
"github.com/roasbeef/btcd/rpcclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createNewNotifier creates a new instance of the ChainNotifier interface
|
||||||
|
// implemented by BitcoindNotifier.
|
||||||
|
func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
|
||||||
|
if len(args) != 3 {
|
||||||
|
return nil, fmt.Errorf("incorrect number of arguments to "+
|
||||||
|
".New(...), expected 3, instead passed %v", len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
config, ok := args[0].(*rpcclient.ConnConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("first argument to bitcoindnotifier." +
|
||||||
|
"New is incorrect, expected a *rpcclient.ConnConfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
zmqConnect, ok := args[1].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("second argument to bitcoindnotifier." +
|
||||||
|
"New is incorrect, expected a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
params, ok := args[2].(chaincfg.Params)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("third argument to bitcoindnotifier." +
|
||||||
|
"New is incorrect, expected a chaincfg.Params")
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(config, zmqConnect, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init registers a driver for the BtcdNotifier concrete implementation of the
|
||||||
|
// chainntnfs.ChainNotifier interface.
|
||||||
|
func init() {
|
||||||
|
// Register the driver.
|
||||||
|
notifier := &chainntnfs.NotifierDriver{
|
||||||
|
NotifierType: notifierType,
|
||||||
|
New: createNewNotifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chainntnfs.RegisterNotifier(notifier); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register notifier driver '%s': %v",
|
||||||
|
notifierType, err))
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@ -13,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/lightninglabs/neutrino"
|
"github.com/lightninglabs/neutrino"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/ltcsuite/ltcd/btcjson"
|
||||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
"github.com/roasbeef/btcwallet/walletdb"
|
"github.com/roasbeef/btcwallet/walletdb"
|
||||||
|
|
||||||
@ -24,6 +27,10 @@ import (
|
|||||||
"github.com/roasbeef/btcd/wire"
|
"github.com/roasbeef/btcd/wire"
|
||||||
"github.com/roasbeef/btcutil"
|
"github.com/roasbeef/btcutil"
|
||||||
|
|
||||||
|
// Required to auto-register the bitcoind backed ChainNotifier
|
||||||
|
// implementation.
|
||||||
|
_ "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify"
|
||||||
|
|
||||||
// Required to auto-register the btcd backed ChainNotifier
|
// Required to auto-register the btcd backed ChainNotifier
|
||||||
// implementation.
|
// implementation.
|
||||||
_ "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
|
_ "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
|
||||||
@ -32,7 +39,8 @@ import (
|
|||||||
// implementation.
|
// implementation.
|
||||||
_ "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
|
_ "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
|
||||||
|
|
||||||
_ "github.com/roasbeef/btcwallet/walletdb/bdb" // Required to register the boltdb walletdb implementation.
|
// Required to register the boltdb walletdb implementation.
|
||||||
|
_ "github.com/roasbeef/btcwallet/walletdb/bdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -43,7 +51,7 @@ var (
|
|||||||
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
|
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
|
||||||
}
|
}
|
||||||
|
|
||||||
netParams = &chaincfg.SimNetParams
|
netParams = &chaincfg.RegressionNetParams
|
||||||
privKey, pubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey)
|
privKey, pubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey)
|
||||||
addrPk, _ = btcutil.NewAddressPubKey(pubKey.SerializeCompressed(),
|
addrPk, _ = btcutil.NewAddressPubKey(pubKey.SerializeCompressed(),
|
||||||
netParams)
|
netParams)
|
||||||
@ -65,6 +73,39 @@ func getTestTxId(miner *rpctest.Harness) (*chainhash.Hash, error) {
|
|||||||
return miner.SendOutputs(outputs, 10)
|
return miner.SendOutputs(outputs, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error {
|
||||||
|
var found bool
|
||||||
|
var tx *btcutil.Tx
|
||||||
|
var err error
|
||||||
|
timeout := time.After(10 * time.Second)
|
||||||
|
for !found {
|
||||||
|
// Do a short wait
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
return fmt.Errorf("timeout after 10s")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check for the harness' knowledge of the txid
|
||||||
|
tx, err = r.Node.GetRawTransaction(txid)
|
||||||
|
if err != nil {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *btcjson.RPCError:
|
||||||
|
if e.Code == btcjson.ErrRPCNoTxInfo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tx != nil && tx.MsgTx().TxHash() == *txid {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func testSingleConfirmationNotification(miner *rpctest.Harness,
|
func testSingleConfirmationNotification(miner *rpctest.Harness,
|
||||||
notifier chainntnfs.ChainNotifier, t *testing.T) {
|
notifier chainntnfs.ChainNotifier, t *testing.T) {
|
||||||
|
|
||||||
@ -80,6 +121,11 @@ func testSingleConfirmationNotification(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err := miner.Node.GetBestBlock()
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -143,6 +189,11 @@ func testMultiConfirmationNotification(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test addr: %v", err)
|
t.Fatalf("unable to create test addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err := miner.Node.GetBestBlock()
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -201,6 +252,11 @@ func testBatchConfirmationNotification(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to register ntfn: %v", err)
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
}
|
}
|
||||||
confIntents[i] = confIntent
|
confIntents[i] = confIntent
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialConfHeight := uint32(currentHeight + 1)
|
initialConfHeight := uint32(currentHeight + 1)
|
||||||
@ -252,6 +308,11 @@ func createSpendableOutput(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test addr: %v", err)
|
t.Fatalf("unable to create test addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Mine a single block which should include that txid above.
|
// Mine a single block which should include that txid above.
|
||||||
if _, err := miner.Node.Generate(1); err != nil {
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
t.Fatalf("unable to generate single block: %v", err)
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
@ -342,6 +403,11 @@ func testSpendNotification(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to broadcast tx: %v", err)
|
t.Fatalf("unable to broadcast tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, spenderSha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Now we mine a single block, which should include our spend. The
|
// Now we mine a single block, which should include our spend. The
|
||||||
// notification should also be sent off.
|
// notification should also be sent off.
|
||||||
if _, err := miner.Node.Generate(1); err != nil {
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
@ -453,6 +519,11 @@ func testMultiClientConfirmationNotification(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
const (
|
const (
|
||||||
numConfsClients = 5
|
numConfsClients = 5
|
||||||
@ -514,6 +585,11 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate another block containing tx 3, but we won't register conf
|
// Generate another block containing tx 3, but we won't register conf
|
||||||
// notifications for this tx until much later. The notifier must check
|
// notifications for this tx until much later. The notifier must check
|
||||||
// older blocks when the confirmation event is registered below to ensure
|
// older blocks when the confirmation event is registered below to ensure
|
||||||
@ -529,11 +605,21 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
txid2, err := getTestTxId(miner)
|
txid2, err := getTestTxId(miner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err := miner.Node.GetBestBlock()
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -654,6 +740,11 @@ func testLazyNtfnConsumer(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err := miner.Node.GetBestBlock()
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -686,6 +777,11 @@ func testLazyNtfnConsumer(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err = miner.Node.GetBestBlock()
|
_, currentHeight, err = miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -736,6 +832,11 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to create test addr: %v", err)
|
t.Fatalf("unable to create test addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Mine a single block which should include that txid above.
|
// Mine a single block which should include that txid above.
|
||||||
if _, err := miner.Node.Generate(1); err != nil {
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
t.Fatalf("unable to generate single block: %v", err)
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
@ -789,6 +890,11 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to brodacst tx: %v", err)
|
t.Fatalf("unable to brodacst tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, spenderSha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Now we mine an additional block, which should include our spend.
|
// Now we mine an additional block, which should include our spend.
|
||||||
if _, err := miner.Node.Generate(1); err != nil {
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
t.Fatalf("unable to generate single block: %v", err)
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
@ -877,6 +983,11 @@ func testCancelSpendNtfn(node *rpctest.Harness,
|
|||||||
t.Fatalf("unable to brodacst tx: %v", err)
|
t.Fatalf("unable to brodacst tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(node, spenderSha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Now we mine a single block, which should include our spend. The
|
// Now we mine a single block, which should include our spend. The
|
||||||
// notification should also be sent off.
|
// notification should also be sent off.
|
||||||
if _, err := node.Node.Generate(1); err != nil {
|
if _, err := node.Node.Generate(1); err != nil {
|
||||||
@ -1021,6 +1132,11 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier,
|
|||||||
t.Fatalf("unable to create test tx: %v", err)
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, currentHeight, err := miner.Node.GetBestBlock()
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get current height: %v", err)
|
t.Fatalf("unable to get current height: %v", err)
|
||||||
@ -1094,11 +1210,16 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.ChainNotifier,
|
|||||||
t.Fatalf("unable to get raw tx: %v", err)
|
t.Fatalf("unable to get raw tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = miner2.Node.SendRawTransaction(tx.MsgTx(), false)
|
txid, err = miner2.Node.SendRawTransaction(tx.MsgTx(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get send tx: %v", err)
|
t.Fatalf("unable to get send tx: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = miner.Node.Generate(3)
|
_, err = miner.Node.Generate(3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate single block: %v", err)
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
@ -1206,12 +1327,73 @@ func TestInterfaces(t *testing.T) {
|
|||||||
|
|
||||||
switch notifierType {
|
switch notifierType {
|
||||||
|
|
||||||
|
case "bitcoind":
|
||||||
|
// Start a bitcoind instance.
|
||||||
|
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket"
|
||||||
|
cleanUp1 := func() {
|
||||||
|
os.RemoveAll(tempBitcoindDir)
|
||||||
|
}
|
||||||
|
cleanUp = cleanUp1
|
||||||
|
rpcPort := rand.Int()%(65536-1024) + 1024
|
||||||
|
bitcoind := exec.Command(
|
||||||
|
"bitcoind",
|
||||||
|
"-datadir="+tempBitcoindDir,
|
||||||
|
"-regtest",
|
||||||
|
"-connect="+p2pAddr,
|
||||||
|
"-txindex",
|
||||||
|
"-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+
|
||||||
|
"d$507c670e800a95284294edb5773b05544b"+
|
||||||
|
"220110063096c221be9933c82d38e1",
|
||||||
|
fmt.Sprintf("-rpcport=%d", rpcPort),
|
||||||
|
"-disablewallet",
|
||||||
|
"-zmqpubrawblock="+zmqPath,
|
||||||
|
"-zmqpubrawtx="+zmqPath,
|
||||||
|
)
|
||||||
|
err = bitcoind.Start()
|
||||||
|
if err != nil {
|
||||||
|
cleanUp1()
|
||||||
|
t.Fatalf("Couldn't start bitcoind: %v", err)
|
||||||
|
}
|
||||||
|
cleanUp2 := func() {
|
||||||
|
bitcoind.Process.Kill()
|
||||||
|
bitcoind.Wait()
|
||||||
|
cleanUp1()
|
||||||
|
}
|
||||||
|
cleanUp = cleanUp2
|
||||||
|
|
||||||
|
// Wait for the bitcoind instance to start up.
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// Start the FilteredChainView implementation instance.
|
||||||
|
config := rpcclient.ConnConfig{
|
||||||
|
Host: fmt.Sprintf(
|
||||||
|
"127.0.0.1:%d", rpcPort),
|
||||||
|
User: "weks",
|
||||||
|
Pass: "weks",
|
||||||
|
DisableAutoReconnect: false,
|
||||||
|
DisableConnectOnNew: true,
|
||||||
|
DisableTLS: true,
|
||||||
|
HTTPPostMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
notifier, err = notifierDriver.New(&config, zmqPath,
|
||||||
|
*netParams)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create %v notifier: %v",
|
||||||
|
notifierType, err)
|
||||||
|
}
|
||||||
|
|
||||||
case "btcd":
|
case "btcd":
|
||||||
notifier, err = notifierDriver.New(&rpcConfig)
|
notifier, err = notifierDriver.New(&rpcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create %v notifier: %v",
|
t.Fatalf("unable to create %v notifier: %v",
|
||||||
notifierType, err)
|
notifierType, err)
|
||||||
}
|
}
|
||||||
|
cleanUp = func() {}
|
||||||
|
|
||||||
case "neutrino":
|
case "neutrino":
|
||||||
spvDir, err := ioutil.TempDir("", "neutrino")
|
spvDir, err := ioutil.TempDir("", "neutrino")
|
||||||
|
6
glide.lock
generated
6
glide.lock
generated
@ -1,5 +1,5 @@
|
|||||||
hash: 8438e391bed32638a8e14402992af50b7656b380f69741cf8422584c2e1f2b31
|
hash: d145c16f2f9cfdf4937eb8b7cdd65919a8c351593a179acc23f2cbca5b42f34b
|
||||||
updated: 2017-12-13T15:13:22.311098343-08:00
|
updated: 2017-12-22T23:45:25.148488338-07:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/aead/chacha20
|
- name: github.com/aead/chacha20
|
||||||
version: d31a916ded42d1640b9d89a26f8abd53cc96790c
|
version: d31a916ded42d1640b9d89a26f8abd53cc96790c
|
||||||
@ -86,6 +86,8 @@ imports:
|
|||||||
version: 946bd9fbed05568b0f3cd188353d8aa28f38b688
|
version: 946bd9fbed05568b0f3cd188353d8aa28f38b688
|
||||||
subpackages:
|
subpackages:
|
||||||
- internal/socket
|
- internal/socket
|
||||||
|
- name: github.com/pebbe/zmq4
|
||||||
|
version: 90d69e412a09549f2e90bac70fbb449081f1e5c1
|
||||||
- name: github.com/roasbeef/btcd
|
- name: github.com/roasbeef/btcd
|
||||||
version: 9978b939c33973be19b932fa7b936079bb7ba38d
|
version: 9978b939c33973be19b932fa7b936079bb7ba38d
|
||||||
subpackages:
|
subpackages:
|
||||||
|
@ -24,6 +24,7 @@ import:
|
|||||||
- txscript
|
- txscript
|
||||||
- wire
|
- wire
|
||||||
- connmgr
|
- connmgr
|
||||||
|
- package: github.com/pebbe/zmq4
|
||||||
- package: github.com/roasbeef/btcrpcclient
|
- package: github.com/roasbeef/btcrpcclient
|
||||||
version: d0f4db8b4dad0ca3d569b804f21247c3dd96acbb
|
version: d0f4db8b4dad0ca3d569b804f21247c3dd96acbb
|
||||||
- package: github.com/roasbeef/btcutil
|
- package: github.com/roasbeef/btcutil
|
||||||
|
@ -29,23 +29,7 @@ var (
|
|||||||
//
|
//
|
||||||
// This method is a part of the lnwallet.BlockChainIO interface.
|
// This method is a part of the lnwallet.BlockChainIO interface.
|
||||||
func (b *BtcWallet) GetBestBlock() (*chainhash.Hash, int32, error) {
|
func (b *BtcWallet) GetBestBlock() (*chainhash.Hash, int32, error) {
|
||||||
switch backend := b.chain.(type) {
|
return b.chain.GetBestBlock()
|
||||||
|
|
||||||
case *chain.NeutrinoClient:
|
|
||||||
header, height, err := backend.CS.BlockHeaders.ChainTip()
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockHash := header.BlockHash()
|
|
||||||
return &blockHash, int32(height), nil
|
|
||||||
|
|
||||||
case *chain.RPCClient:
|
|
||||||
return backend.GetBestBlock()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, -1, fmt.Errorf("unknown backend")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUtxo returns the original output referenced by the passed outpoint.
|
// GetUtxo returns the original output referenced by the passed outpoint.
|
||||||
@ -100,6 +84,26 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, heightHint uint32) (*wire.TxOut,
|
|||||||
PkScript: pkScript,
|
PkScript: pkScript,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case *chain.BitcoindClient:
|
||||||
|
txout, err := backend.GetTxOut(&op.Hash, op.Index, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if txout == nil {
|
||||||
|
return nil, ErrOutputSpent
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := hex.DecodeString(txout.ScriptPubKey.Hex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wire.TxOut{
|
||||||
|
// Sadly, gettxout returns the output value in BTC
|
||||||
|
// instead of satoshis.
|
||||||
|
Value: int64(txout.Value * 1e8),
|
||||||
|
PkScript: pkScript,
|
||||||
|
}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown backend")
|
return nil, fmt.Errorf("unknown backend")
|
||||||
}
|
}
|
||||||
@ -109,27 +113,7 @@ func (b *BtcWallet) GetUtxo(op *wire.OutPoint, heightHint uint32) (*wire.TxOut,
|
|||||||
//
|
//
|
||||||
// This method is a part of the lnwallet.BlockChainIO interface.
|
// This method is a part of the lnwallet.BlockChainIO interface.
|
||||||
func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
|
func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
|
||||||
switch backend := b.chain.(type) {
|
return b.chain.GetBlock(blockHash)
|
||||||
|
|
||||||
case *chain.NeutrinoClient:
|
|
||||||
block, err := backend.CS.GetBlockFromNetwork(*blockHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return block.MsgBlock(), nil
|
|
||||||
|
|
||||||
case *chain.RPCClient:
|
|
||||||
block, err := backend.GetBlock(blockHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return block, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown backend")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockHash returns the hash of the block in the best blockchain at the
|
// GetBlockHash returns the hash of the block in the best blockchain at the
|
||||||
@ -137,29 +121,7 @@ func (b *BtcWallet) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error)
|
|||||||
//
|
//
|
||||||
// This method is a part of the lnwallet.BlockChainIO interface.
|
// This method is a part of the lnwallet.BlockChainIO interface.
|
||||||
func (b *BtcWallet) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) {
|
func (b *BtcWallet) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) {
|
||||||
switch backend := b.chain.(type) {
|
return b.chain.GetBlockHash(blockHeight)
|
||||||
|
|
||||||
case *chain.NeutrinoClient:
|
|
||||||
height := uint32(blockHeight)
|
|
||||||
blockHeader, err := backend.CS.BlockHeaders.FetchHeaderByHeight(height)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockHash := blockHeader.BlockHash()
|
|
||||||
return &blockHash, nil
|
|
||||||
|
|
||||||
case *chain.RPCClient:
|
|
||||||
blockHash, err := backend.GetBlockHash(blockHeight)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return blockHash, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown backend")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compile time check to ensure that BtcWallet implements the BlockChainIO
|
// A compile time check to ensure that BtcWallet implements the BlockChainIO
|
||||||
|
@ -119,6 +119,17 @@ func New(cfg Config) (*BtcWallet, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackEnd returns the underlying ChainService's name as a string.
|
||||||
|
//
|
||||||
|
// This is a part of the WalletController interface.
|
||||||
|
func (b *BtcWallet) BackEnd() string {
|
||||||
|
if b.chain != nil {
|
||||||
|
return b.chain.BackEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Start initializes the underlying rpc connection, the wallet itself, and
|
// Start initializes the underlying rpc connection, the wallet itself, and
|
||||||
// begins syncing to the current available blockchain state.
|
// begins syncing to the current available blockchain state.
|
||||||
//
|
//
|
||||||
@ -668,22 +679,9 @@ func (b *BtcWallet) IsSynced() (bool, error) {
|
|||||||
|
|
||||||
// Next, query the chain backend to grab the info about the tip of the
|
// Next, query the chain backend to grab the info about the tip of the
|
||||||
// main chain.
|
// main chain.
|
||||||
switch backend := b.cfg.ChainSource.(type) {
|
bestHash, bestHeight, err = b.cfg.ChainSource.GetBestBlock()
|
||||||
case *chain.NeutrinoClient:
|
if err != nil {
|
||||||
header, height, err := backend.CS.BlockHeaders.ChainTip()
|
return false, err
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bh := header.BlockHash()
|
|
||||||
bestHash = &bh
|
|
||||||
bestHeight = int32(height)
|
|
||||||
|
|
||||||
case *chain.RPCClient:
|
|
||||||
bestHash, bestHeight, err = backend.GetBestBlock()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the wallet hasn't yet fully synced to the node's best chain tip,
|
// If the wallet hasn't yet fully synced to the node's best chain tip,
|
||||||
@ -696,21 +694,9 @@ func (b *BtcWallet) IsSynced() (bool, error) {
|
|||||||
// still may not yet be synced as the chain backend may still be
|
// still may not yet be synced as the chain backend may still be
|
||||||
// catching up to the main chain. So we'll grab the block header in
|
// catching up to the main chain. So we'll grab the block header in
|
||||||
// order to make a guess based on the current time stamp.
|
// order to make a guess based on the current time stamp.
|
||||||
var blockHeader *wire.BlockHeader
|
blockHeader, err := b.cfg.ChainSource.GetBlockHeader(bestHash)
|
||||||
switch backend := b.cfg.ChainSource.(type) {
|
if err != nil {
|
||||||
|
return false, err
|
||||||
case *chain.NeutrinoClient:
|
|
||||||
bh, _, err := backend.CS.BlockHeaders.FetchHeader(bestHash)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
blockHeader = bh
|
|
||||||
|
|
||||||
case *chain.RPCClient:
|
|
||||||
blockHeader, err = backend.GetBlockHeader(bestHash)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the timestamp no the best header is more than 2 hours in the
|
// If the timestamp no the best header is more than 2 hours in the
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/roasbeef/btcwallet/chain"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -36,6 +37,7 @@ func init() {
|
|||||||
driver := &lnwallet.WalletDriver{
|
driver := &lnwallet.WalletDriver{
|
||||||
WalletType: walletType,
|
WalletType: walletType,
|
||||||
New: createNewWallet,
|
New: createNewWallet,
|
||||||
|
BackEnds: chain.BackEnds,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := lnwallet.RegisterWallet(driver); err != nil {
|
if err := lnwallet.RegisterWallet(driver); err != nil {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package lnwallet
|
package lnwallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/roasbeef/btcd/blockchain"
|
"github.com/roasbeef/btcd/blockchain"
|
||||||
"github.com/roasbeef/btcd/rpcclient"
|
"github.com/roasbeef/btcd/rpcclient"
|
||||||
"github.com/roasbeef/btcutil"
|
"github.com/roasbeef/btcutil"
|
||||||
@ -200,3 +202,145 @@ func (b *BtcdFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amou
|
|||||||
// A compile-time assertion to ensure that BtcdFeeEstimator implements the
|
// A compile-time assertion to ensure that BtcdFeeEstimator implements the
|
||||||
// FeeEstimator interface.
|
// FeeEstimator interface.
|
||||||
var _ FeeEstimator = (*BtcdFeeEstimator)(nil)
|
var _ FeeEstimator = (*BtcdFeeEstimator)(nil)
|
||||||
|
|
||||||
|
// BitcoindFeeEstimator is an implementation of the FeeEstimator interface
|
||||||
|
// backed by the RPC interface of an active bitcoind node. This implementation
|
||||||
|
// will proxy any fee estimation requests to bitcoind's RPC interace.
|
||||||
|
type BitcoindFeeEstimator struct {
|
||||||
|
// fallBackFeeRate is the fall back fee rate in satoshis per byte that
|
||||||
|
// is returned if the fee estimator does not yet have enough data to
|
||||||
|
// actually produce fee estimates.
|
||||||
|
fallBackFeeRate btcutil.Amount
|
||||||
|
|
||||||
|
bitcoindConn *rpcclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitcoindFeeEstimator creates a new BitcoindFeeEstimator given a fully
|
||||||
|
// populated rpc config that is able to successfully connect and authenticate
|
||||||
|
// with the bitcoind node, and also a fall back fee rate. The fallback fee rate
|
||||||
|
// is used in the occasion that the estimator has insufficient data, or returns
|
||||||
|
// zero for a fee estimate.
|
||||||
|
func NewBitcoindFeeEstimator(rpcConfig rpcclient.ConnConfig,
|
||||||
|
fallBackFeeRate btcutil.Amount) (*BitcoindFeeEstimator, error) {
|
||||||
|
|
||||||
|
rpcConfig.DisableConnectOnNew = true
|
||||||
|
rpcConfig.DisableAutoReconnect = false
|
||||||
|
rpcConfig.DisableTLS = true
|
||||||
|
rpcConfig.HTTPPostMode = true
|
||||||
|
chainConn, err := rpcclient.New(&rpcConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BitcoindFeeEstimator{
|
||||||
|
fallBackFeeRate: fallBackFeeRate,
|
||||||
|
bitcoindConn: chainConn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start signals the FeeEstimator to start any processes or goroutines
|
||||||
|
// it needs to perform its duty.
|
||||||
|
//
|
||||||
|
// NOTE: This method is part of the FeeEstimator interface.
|
||||||
|
func (b *BitcoindFeeEstimator) Start() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops any spawned goroutines and cleans up the resources used
|
||||||
|
// by the fee estimator.
|
||||||
|
//
|
||||||
|
// NOTE: This method is part of the FeeEstimator interface.
|
||||||
|
func (b *BitcoindFeeEstimator) Stop() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateFeePerByte takes in a target for the number of blocks until an
|
||||||
|
// initial confirmation and returns the estimated fee expressed in
|
||||||
|
// satoshis/byte.
|
||||||
|
func (b *BitcoindFeeEstimator) EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) {
|
||||||
|
feeEstimate, err := b.fetchEstimatePerByte(numBlocks)
|
||||||
|
switch {
|
||||||
|
// If the estimator doesn't have enough data, or returns an error, then
|
||||||
|
// to return a proper value, then we'll return the default fall back
|
||||||
|
// fee rate.
|
||||||
|
case err != nil:
|
||||||
|
walletLog.Errorf("unable to query estimator: %v", err)
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case feeEstimate == 0:
|
||||||
|
return b.fallBackFeeRate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return feeEstimate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateFeePerWeight takes in a target for the number of blocks until an
|
||||||
|
// initial confirmation and returns the estimated fee expressed in
|
||||||
|
// satoshis/weight.
|
||||||
|
func (b *BitcoindFeeEstimator) EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) {
|
||||||
|
feePerByte, err := b.EstimateFeePerByte(numBlocks)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll scale down the fee per byte to fee per weight, as for each raw
|
||||||
|
// byte, there's 1/4 unit of weight mapped to it.
|
||||||
|
satWeight := feePerByte / blockchain.WitnessScaleFactor
|
||||||
|
|
||||||
|
// If this ends up scaling down to a zero sat/weight amount, then we'll
|
||||||
|
// use the default fallback fee rate.
|
||||||
|
// TODO(aakselrod): maybe use the per-byte rate if it's non-zero?
|
||||||
|
// Otherwise, we can return a higher sat/byte than sat/weight.
|
||||||
|
if satWeight == 0 {
|
||||||
|
return b.fallBackFeeRate / blockchain.WitnessScaleFactor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return satWeight, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchEstimate returns a fee estimate for a transaction be be confirmed in
|
||||||
|
// confTarget blocks. The estimate is returned in sat/byte.
|
||||||
|
func (b *BitcoindFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amount, error) {
|
||||||
|
// First, we'll send an "estimatesmartfee" command as a raw request,
|
||||||
|
// since it isn't supported by btcd but is available in bitcoind.
|
||||||
|
target, err := json.Marshal(uint64(confTarget))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// TODO: Allow selection of economical/conservative modifiers.
|
||||||
|
resp, err := b.bitcoindConn.RawRequest("estimatesmartfee",
|
||||||
|
[]json.RawMessage{target})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll parse the response to get the BTC per KB.
|
||||||
|
feeEstimate := struct {
|
||||||
|
Feerate float64 `json:"feerate"`
|
||||||
|
}{}
|
||||||
|
err = json.Unmarshal(resp, &feeEstimate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll convert the returned value to satoshis, as it's
|
||||||
|
// currently returned in BTC.
|
||||||
|
satPerKB, err := btcutil.NewAmount(feeEstimate.Feerate)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The value returned is expressed in fees per KB, while we want
|
||||||
|
// fee-per-byte, so we'll divide by 1024 to map to satoshis-per-byte
|
||||||
|
// before returning the estimate.
|
||||||
|
satPerByte := satPerKB / 1024
|
||||||
|
|
||||||
|
walletLog.Debugf("Returning %v sat/byte for conf target of %v",
|
||||||
|
int64(satPerByte), confTarget)
|
||||||
|
|
||||||
|
return satPerByte, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile-time assertion to ensure that BitcoindFeeEstimator implements the
|
||||||
|
// FeeEstimator interface.
|
||||||
|
var _ FeeEstimator = (*BitcoindFeeEstimator)(nil)
|
||||||
|
@ -207,6 +207,11 @@ type WalletController interface {
|
|||||||
// Stop signals the wallet for shutdown. Shutdown may entail closing
|
// Stop signals the wallet for shutdown. Shutdown may entail closing
|
||||||
// any active sockets, database handles, stopping goroutines, etc.
|
// any active sockets, database handles, stopping goroutines, etc.
|
||||||
Stop() error
|
Stop() error
|
||||||
|
|
||||||
|
// BackEnd returns a name for the wallet's backing chain service,
|
||||||
|
// which could be e.g. btcd, bitcoind, neutrino, or another consensus
|
||||||
|
// service.
|
||||||
|
BackEnd() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockChainIO is a dedicated source which will be used to obtain queries
|
// BlockChainIO is a dedicated source which will be used to obtain queries
|
||||||
@ -288,6 +293,10 @@ type WalletDriver struct {
|
|||||||
// initialization flexibility, thereby accommodating several potential
|
// initialization flexibility, thereby accommodating several potential
|
||||||
// WalletController implementations.
|
// WalletController implementations.
|
||||||
New func(args ...interface{}) (WalletController, error)
|
New func(args ...interface{}) (WalletController, error)
|
||||||
|
|
||||||
|
// BackEnds returns a list of available chain service drivers for the
|
||||||
|
// wallet driver. This could be e.g. bitcoind, btcd, neutrino, etc.
|
||||||
|
BackEnds func() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -5,8 +5,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -17,7 +19,10 @@ import (
|
|||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/neutrino"
|
||||||
"github.com/roasbeef/btcwallet/chain"
|
"github.com/roasbeef/btcwallet/chain"
|
||||||
|
"github.com/roasbeef/btcwallet/walletdb"
|
||||||
|
_ "github.com/roasbeef/btcwallet/walletdb/bdb"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
|
"github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
|
||||||
@ -25,10 +30,10 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/roasbeef/btcd/btcjson"
|
||||||
"github.com/roasbeef/btcd/chaincfg"
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
"github.com/roasbeef/btcd/rpcclient"
|
"github.com/roasbeef/btcd/rpcclient"
|
||||||
_ "github.com/roasbeef/btcwallet/walletdb/bdb"
|
|
||||||
|
|
||||||
"github.com/roasbeef/btcd/btcec"
|
"github.com/roasbeef/btcd/btcec"
|
||||||
"github.com/roasbeef/btcd/integration/rpctest"
|
"github.com/roasbeef/btcd/integration/rpctest"
|
||||||
@ -76,7 +81,7 @@ var (
|
|||||||
0x69, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
0x69, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
||||||
}
|
}
|
||||||
|
|
||||||
netParams = &chaincfg.SimNetParams
|
netParams = &chaincfg.RegressionNetParams
|
||||||
chainHash = netParams.GenesisHash
|
chainHash = netParams.GenesisHash
|
||||||
|
|
||||||
_, alicePub = btcec.PrivKeyFromBytes(btcec.S256(), testHdSeed[:])
|
_, alicePub = btcec.PrivKeyFromBytes(btcec.S256(), testHdSeed[:])
|
||||||
@ -145,6 +150,12 @@ func calcStaticFee(numHTLCs int) btcutil.Amount {
|
|||||||
func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet,
|
func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet,
|
||||||
numOutputs, btcPerOutput int) error {
|
numOutputs, btcPerOutput int) error {
|
||||||
|
|
||||||
|
// For initial neutrino connection, wait a second.
|
||||||
|
// TODO(aakselrod): Eliminate the need for this.
|
||||||
|
switch w.BackEnd() {
|
||||||
|
case "neutrino":
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
// Using the mining node, spend from a coinbase output numOutputs to
|
// Using the mining node, spend from a coinbase output numOutputs to
|
||||||
// give us btcPerOutput with each output.
|
// give us btcPerOutput with each output.
|
||||||
satoshiPerOutput := int64(btcPerOutput * 1e8)
|
satoshiPerOutput := int64(btcPerOutput * 1e8)
|
||||||
@ -188,6 +199,7 @@ func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
|
|
||||||
// Wait until the wallet has finished syncing up to the main chain.
|
// Wait until the wallet has finished syncing up to the main chain.
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
timeout := time.After(30 * time.Second)
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
balance, err := w.ConfirmedBalance(1, false)
|
balance, err := w.ConfirmedBalance(1, false)
|
||||||
@ -197,6 +209,17 @@ func loadTestCredits(miner *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
if balance == expectedBalance {
|
if balance == expectedBalance {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
synced, err := w.IsSynced()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("timed out after 30 seconds "+
|
||||||
|
"waiting for balance %v, current balance %v, "+
|
||||||
|
"synced: %t", expectedBalance, balance, synced)
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
|
||||||
@ -222,7 +245,7 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness,
|
|||||||
WalletController: wc,
|
WalletController: wc,
|
||||||
Signer: signer,
|
Signer: signer,
|
||||||
ChainIO: bio,
|
ChainIO: bio,
|
||||||
FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250},
|
FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 10},
|
||||||
DefaultConstraints: channeldb.ChannelConstraints{
|
DefaultConstraints: channeldb.ChannelConstraints{
|
||||||
DustLimit: 500,
|
DustLimit: 500,
|
||||||
MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100,
|
MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100,
|
||||||
@ -343,6 +366,9 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
|
|||||||
bobFundingSigs, bobCommitSig,
|
bobFundingSigs, bobCommitSig,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
for _, in := range aliceChanReservation.FinalFundingTx().TxIn {
|
||||||
|
fmt.Println(in.PreviousOutPoint.String())
|
||||||
|
}
|
||||||
t.Fatalf("unable to consume alice's sigs: %v", err)
|
t.Fatalf("unable to consume alice's sigs: %v", err)
|
||||||
}
|
}
|
||||||
_, err = bobChanReservation.CompleteReservation(
|
_, err = bobChanReservation.CompleteReservation(
|
||||||
@ -384,6 +410,10 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// Mine a single block, the funding transaction should be included
|
// Mine a single block, the funding transaction should be included
|
||||||
// within this block.
|
// within this block.
|
||||||
|
err = waitForMempoolTx(miner, &fundingSha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
blockHashes, err := miner.Node.Generate(1)
|
blockHashes, err := miner.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
@ -402,6 +432,16 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
|
|||||||
|
|
||||||
assertReservationDeleted(aliceChanReservation, t)
|
assertReservationDeleted(aliceChanReservation, t)
|
||||||
assertReservationDeleted(bobChanReservation, t)
|
assertReservationDeleted(bobChanReservation, t)
|
||||||
|
|
||||||
|
// Wait for wallets to catch up to prevent issues in subsequent tests.
|
||||||
|
err = waitForWalletSync(miner, alice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to sync alice: %v", err)
|
||||||
|
}
|
||||||
|
err = waitForWalletSync(miner, bob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to sync bob: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFundingTransactionLockedOutputs(miner *rpctest.Harness,
|
func testFundingTransactionLockedOutputs(miner *rpctest.Harness,
|
||||||
@ -759,6 +799,10 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// Mine a single block, the funding transaction should be included
|
// Mine a single block, the funding transaction should be included
|
||||||
// within this block.
|
// within this block.
|
||||||
|
err = waitForMempoolTx(miner, &fundingSha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
blockHashes, err := miner.Node.Generate(1)
|
blockHashes, err := miner.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
@ -768,7 +812,8 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to find block: %v", err)
|
t.Fatalf("unable to find block: %v", err)
|
||||||
}
|
}
|
||||||
if len(block.Transactions) != 2 {
|
if len(block.Transactions) != 2 {
|
||||||
t.Fatalf("funding transaction wasn't mined: %v", err)
|
t.Fatalf("funding transaction wasn't mined: %d",
|
||||||
|
len(block.Transactions))
|
||||||
}
|
}
|
||||||
blockTx := block.Transactions[1]
|
blockTx := block.Transactions[1]
|
||||||
if blockTx.TxHash() != fundingSha {
|
if blockTx.TxHash() != fundingSha {
|
||||||
@ -815,8 +860,10 @@ func testListTransactionDetails(miner *rpctest.Harness,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next, fetch all the current transaction details.
|
// Next, fetch all the current transaction details.
|
||||||
// TODO(roasbeef): use ntfn client here instead?
|
err = waitForWalletSync(miner, alice)
|
||||||
time.Sleep(time.Second * 2)
|
if err != nil {
|
||||||
|
t.Fatalf("Couldn't sync Alice's wallet: %v", err)
|
||||||
|
}
|
||||||
txDetails, err := alice.ListTransactionDetails()
|
txDetails, err := alice.ListTransactionDetails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch tx details: %v", err)
|
t.Fatalf("unable to fetch tx details: %v", err)
|
||||||
@ -905,6 +952,10 @@ func testListTransactionDetails(miner *rpctest.Harness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create burn tx: %v", err)
|
t.Fatalf("unable to create burn tx: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(miner, burnTXID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
burnBlock, err := miner.Node.Generate(1)
|
burnBlock, err := miner.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to mine block: %v", err)
|
t.Fatalf("unable to mine block: %v", err)
|
||||||
@ -912,7 +963,10 @@ func testListTransactionDetails(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// Fetch the transaction details again, the new transaction should be
|
// Fetch the transaction details again, the new transaction should be
|
||||||
// shown as debiting from the wallet's balance.
|
// shown as debiting from the wallet's balance.
|
||||||
time.Sleep(time.Second * 2)
|
err = waitForWalletSync(miner, alice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Couldn't sync Alice's wallet: %v", err)
|
||||||
|
}
|
||||||
txDetails, err = alice.ListTransactionDetails()
|
txDetails, err = alice.ListTransactionDetails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch tx details: %v", err)
|
t.Fatalf("unable to fetch tx details: %v", err)
|
||||||
@ -955,7 +1009,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
|
|||||||
// implementation of the WalletController.
|
// implementation of the WalletController.
|
||||||
txClient, err := alice.SubscribeTransactions()
|
txClient, err := alice.SubscribeTransactions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate tx subscription: %v", err)
|
t.Skipf("unable to generate tx subscription: %v", err)
|
||||||
}
|
}
|
||||||
defer txClient.Cancel()
|
defer txClient.Cancel()
|
||||||
|
|
||||||
@ -964,25 +1018,33 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
|
|||||||
numTxns = 3
|
numTxns = 3
|
||||||
)
|
)
|
||||||
unconfirmedNtfns := make(chan struct{})
|
unconfirmedNtfns := make(chan struct{})
|
||||||
go func() {
|
switch alice.BackEnd() {
|
||||||
for i := 0; i < numTxns; i++ {
|
case "neutrino":
|
||||||
txDetail := <-txClient.UnconfirmedTransactions()
|
// Neutrino doesn't listen for unconfirmed transactions.
|
||||||
if txDetail.NumConfirmations != 0 {
|
default:
|
||||||
t.Fatalf("incorrect number of confs, expected %v got %v",
|
go func() {
|
||||||
0, txDetail.NumConfirmations)
|
for i := 0; i < numTxns; i++ {
|
||||||
|
txDetail := <-txClient.UnconfirmedTransactions()
|
||||||
|
if txDetail.NumConfirmations != 0 {
|
||||||
|
t.Fatalf("incorrect number of confs, "+
|
||||||
|
"expected %v got %v", 0,
|
||||||
|
txDetail.NumConfirmations)
|
||||||
|
}
|
||||||
|
if txDetail.Value != outputAmt {
|
||||||
|
t.Fatalf("incorrect output amt, "+
|
||||||
|
"expected %v got %v", outputAmt,
|
||||||
|
txDetail.Value)
|
||||||
|
}
|
||||||
|
if txDetail.BlockHash != nil {
|
||||||
|
t.Fatalf("block hash should be nil, "+
|
||||||
|
"is instead %v",
|
||||||
|
txDetail.BlockHash)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if txDetail.Value != outputAmt {
|
|
||||||
t.Fatalf("incorrect output amt, expected %v got %v",
|
|
||||||
outputAmt, txDetail.Value)
|
|
||||||
}
|
|
||||||
if txDetail.BlockHash != nil {
|
|
||||||
t.Fatalf("block hash should be nil, is instead %v",
|
|
||||||
txDetail.BlockHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(unconfirmedNtfns)
|
close(unconfirmedNtfns)
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Next, fetch a fresh address from the wallet, create 3 new outputs
|
// Next, fetch a fresh address from the wallet, create 3 new outputs
|
||||||
// with the pkScript.
|
// with the pkScript.
|
||||||
@ -1000,17 +1062,27 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
|
|||||||
Value: outputAmt,
|
Value: outputAmt,
|
||||||
PkScript: script,
|
PkScript: script,
|
||||||
}
|
}
|
||||||
if _, err := miner.SendOutputs([]*wire.TxOut{output}, 10); err != nil {
|
txid, err := miner.SendOutputs([]*wire.TxOut{output}, 10)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("unable to send coinbase: %v", err)
|
t.Fatalf("unable to send coinbase: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(miner, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should receive a notification for all three transactions
|
switch alice.BackEnd() {
|
||||||
// generated above.
|
case "neutrino":
|
||||||
select {
|
// Neutrino doesn't listen for on unconfirmed transactions.
|
||||||
case <-time.After(time.Second * 5):
|
default:
|
||||||
t.Fatalf("transactions not received after 3 seconds")
|
// We should receive a notification for all three transactions
|
||||||
case <-unconfirmedNtfns: // Fall through on successs
|
// generated above.
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
t.Fatalf("transactions not received after 10 seconds")
|
||||||
|
case <-unconfirmedNtfns: // Fall through on successs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmedNtfns := make(chan struct{})
|
confirmedNtfns := make(chan struct{})
|
||||||
@ -1018,12 +1090,12 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
|
|||||||
for i := 0; i < numTxns; i++ {
|
for i := 0; i < numTxns; i++ {
|
||||||
txDetail := <-txClient.ConfirmedTransactions()
|
txDetail := <-txClient.ConfirmedTransactions()
|
||||||
if txDetail.NumConfirmations != 1 {
|
if txDetail.NumConfirmations != 1 {
|
||||||
t.Fatalf("incorrect number of confs, expected %v got %v",
|
t.Fatalf("incorrect number of confs for %s, expected %v got %v",
|
||||||
1, txDetail.NumConfirmations)
|
txDetail.Hash, 1, txDetail.NumConfirmations)
|
||||||
}
|
}
|
||||||
if txDetail.Value != outputAmt {
|
if txDetail.Value != outputAmt {
|
||||||
t.Fatalf("incorrect output amt, expected %v got %v",
|
t.Fatalf("incorrect output amt, expected %v got %v in txid %s",
|
||||||
outputAmt, txDetail.Value)
|
outputAmt, txDetail.Value, txDetail.Hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(confirmedNtfns)
|
close(confirmedNtfns)
|
||||||
@ -1039,7 +1111,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
|
|||||||
// since they should be mined in the next block.
|
// since they should be mined in the next block.
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 5):
|
case <-time.After(time.Second * 5):
|
||||||
t.Fatalf("transactions not received after 3 seconds")
|
t.Fatalf("transactions not received after 5 seconds")
|
||||||
case <-confirmedNtfns: // Fall through on success
|
case <-confirmedNtfns: // Fall through on success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1088,7 +1160,7 @@ func testSignOutputUsingTweaks(r *rpctest.Harness,
|
|||||||
// generate a regular p2wkh from that.
|
// generate a regular p2wkh from that.
|
||||||
pubkeyHash := btcutil.Hash160(tweakedKey.SerializeCompressed())
|
pubkeyHash := btcutil.Hash160(tweakedKey.SerializeCompressed())
|
||||||
keyAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash,
|
keyAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash,
|
||||||
&chaincfg.SimNetParams)
|
&chaincfg.RegressionNetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create addr: %v", err)
|
t.Fatalf("unable to create addr: %v", err)
|
||||||
}
|
}
|
||||||
@ -1110,6 +1182,10 @@ func testSignOutputUsingTweaks(r *rpctest.Harness,
|
|||||||
|
|
||||||
// Query for the transaction generated above so we can located
|
// Query for the transaction generated above so we can located
|
||||||
// the index of our output.
|
// the index of our output.
|
||||||
|
err = waitForMempoolTx(r, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
tx, err := r.Node.GetRawTransaction(txid)
|
tx, err := r.Node.GetRawTransaction(txid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query for tx: %v", err)
|
t.Fatalf("unable to query for tx: %v", err)
|
||||||
@ -1197,7 +1273,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give wallet time to catch up.
|
// Give wallet time to catch up.
|
||||||
err = waitForWalletSync(w)
|
err = waitForWalletSync(r, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to sync wallet: %v", err)
|
t.Fatalf("unable to sync wallet: %v", err)
|
||||||
}
|
}
|
||||||
@ -1222,16 +1298,21 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
Value: 1e8,
|
Value: 1e8,
|
||||||
PkScript: script,
|
PkScript: script,
|
||||||
}
|
}
|
||||||
if _, err = w.SendOutputs([]*wire.TxOut{output}, 10); err != nil {
|
txid, err := w.SendOutputs([]*wire.TxOut{output}, 10)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("unable to send outputs: %v", err)
|
t.Fatalf("unable to send outputs: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(r, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
_, err = r.Node.Generate(50)
|
_, err = r.Node.Generate(50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate blocks on passed node: %v", err)
|
t.Fatalf("unable to generate blocks on passed node: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give wallet time to catch up.
|
// Give wallet time to catch up.
|
||||||
err = waitForWalletSync(w)
|
err = waitForWalletSync(r, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to sync wallet: %v", err)
|
t.Fatalf("unable to sync wallet: %v", err)
|
||||||
}
|
}
|
||||||
@ -1277,32 +1358,34 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
// one block on the passed miner and two on the created miner,
|
// one block on the passed miner and two on the created miner,
|
||||||
// connecting them, and waiting for them to sync.
|
// connecting them, and waiting for them to sync.
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
peers, err := r2.Node.GetPeerInfo()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get peer info: %v", err)
|
|
||||||
}
|
|
||||||
numPeers := len(peers)
|
|
||||||
err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANRemove)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to disconnect mining nodes: %v", err)
|
|
||||||
}
|
|
||||||
// Wait for disconnection
|
// Wait for disconnection
|
||||||
timeout := time.After(30 * time.Second)
|
timeout := time.After(30 * time.Second)
|
||||||
for true {
|
stillConnected := true
|
||||||
|
var peers []btcjson.GetPeerInfoResult
|
||||||
|
for stillConnected {
|
||||||
// Allow for timeout
|
// Allow for timeout
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
select {
|
select {
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
t.Fatalf("timeout waiting for miner disconnect")
|
t.Fatalf("timeout waiting for miner disconnect")
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANRemove)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to disconnect mining nodes: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
peers, err = r2.Node.GetPeerInfo()
|
peers, err = r2.Node.GetPeerInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get peer info: %v", err)
|
t.Fatalf("unable to get peer info: %v", err)
|
||||||
}
|
}
|
||||||
if len(peers) < numPeers {
|
stillConnected = false
|
||||||
break
|
for _, peer := range peers {
|
||||||
|
if peer.Addr == r.P2PAddress() {
|
||||||
|
stillConnected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
_, err = r.Node.Generate(2)
|
_, err = r.Node.Generate(2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1318,8 +1401,16 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
// Step 5: Reconnect the miners and wait for them to synchronize.
|
// Step 5: Reconnect the miners and wait for them to synchronize.
|
||||||
err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANAdd)
|
err = r2.Node.AddNode(r.P2PAddress(), rpcclient.ANAdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to connect mining nodes together: %v",
|
switch err := err.(type) {
|
||||||
err)
|
case *btcjson.RPCError:
|
||||||
|
if err.Code != -8 {
|
||||||
|
t.Fatalf("unable to connect mining "+
|
||||||
|
"nodes together: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("unable to connect mining nodes "+
|
||||||
|
"together: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = rpctest.JoinNodes([]*rpctest.Harness{r2, r},
|
err = rpctest.JoinNodes([]*rpctest.Harness{r2, r},
|
||||||
rpctest.Blocks)
|
rpctest.Blocks)
|
||||||
@ -1328,7 +1419,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give wallet time to catch up.
|
// Give wallet time to catch up.
|
||||||
err = waitForWalletSync(w)
|
err = waitForWalletSync(r, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to sync wallet: %v", err)
|
t.Fatalf("unable to sync wallet: %v", err)
|
||||||
}
|
}
|
||||||
@ -1405,21 +1496,78 @@ func clearWalletStates(a, b *lnwallet.LightningWallet) error {
|
|||||||
return b.Cfg.Database.Wipe()
|
return b.Cfg.Database.Wipe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForWalletSync(w *lnwallet.LightningWallet) error {
|
func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error {
|
||||||
var synced bool
|
var found bool
|
||||||
|
var tx *btcutil.Tx
|
||||||
var err error
|
var err error
|
||||||
timeout := time.After(10 * time.Second)
|
timeout := time.After(10 * time.Second)
|
||||||
for !synced {
|
for !found {
|
||||||
synced, err = w.IsSynced()
|
// Do a short wait
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
return fmt.Errorf("timeout after 10s")
|
return fmt.Errorf("timeout after 10s")
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check for the harness' knowledge of the txid
|
||||||
|
tx, err = r.Node.GetRawTransaction(txid)
|
||||||
|
if err != nil {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *btcjson.RPCError:
|
||||||
|
if e.Code == btcjson.ErrRPCNoTxInfo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tx != nil && tx.MsgTx().TxHash() == *txid {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error {
|
||||||
|
var synced bool
|
||||||
|
var err error
|
||||||
|
var bestHash, knownHash *chainhash.Hash
|
||||||
|
var bestHeight, knownHeight int32
|
||||||
|
timeout := time.After(10 * time.Second)
|
||||||
|
for !synced {
|
||||||
|
// Do a short wait
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
return fmt.Errorf("timeout after 10s")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check whether the chain source of the wallet is caught up to
|
||||||
|
// the harness it's supposed to be catching up to.
|
||||||
|
bestHash, bestHeight, err = r.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
knownHash, knownHeight, err = w.Cfg.ChainIO.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if knownHeight != bestHeight {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *knownHash != *bestHash {
|
||||||
|
return fmt.Errorf("hash at height %d doesn't match: "+
|
||||||
|
"expected %s, got %s", bestHeight, bestHash,
|
||||||
|
knownHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for synchronization.
|
||||||
|
synced, err = w.IsSynced()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1454,7 +1602,7 @@ func TestLightningWallet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next mine enough blocks in order for segwit and the CSV package
|
// Next mine enough blocks in order for segwit and the CSV package
|
||||||
// soft-fork to activate on SimNet.
|
// soft-fork to activate on RegNet.
|
||||||
numBlocks := netParams.MinerConfirmationWindow * 2
|
numBlocks := netParams.MinerConfirmationWindow * 2
|
||||||
if _, err := miningNode.Node.Generate(numBlocks); err != nil {
|
if _, err := miningNode.Node.Generate(numBlocks); err != nil {
|
||||||
t.Fatalf("unable to generate blocks: %v", err)
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
@ -1470,6 +1618,22 @@ func TestLightningWallet(t *testing.T) {
|
|||||||
t.Fatalf("unable to start notifier: %v", err)
|
t.Fatalf("unable to start notifier: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, walletDriver := range lnwallet.RegisteredWallets() {
|
||||||
|
for _, backEnd := range walletDriver.BackEnds() {
|
||||||
|
runTests(t, walletDriver, backEnd, miningNode,
|
||||||
|
rpcConfig, chainNotifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTests runs all of the tests for a single interface implementation and
|
||||||
|
// chain back-end combination. This makes it easier to use `defer` as well as
|
||||||
|
// factoring out the test logic from the loop which cycles through the
|
||||||
|
// interface implementations.
|
||||||
|
func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||||
|
backEnd string, miningNode *rpctest.Harness,
|
||||||
|
rpcConfig rpcclient.ConnConfig,
|
||||||
|
chainNotifier *btcdnotify.BtcdNotifier) {
|
||||||
var (
|
var (
|
||||||
bio lnwallet.BlockChainIO
|
bio lnwallet.BlockChainIO
|
||||||
|
|
||||||
@ -1478,107 +1642,230 @@ func TestLightningWallet(t *testing.T) {
|
|||||||
|
|
||||||
aliceWalletController lnwallet.WalletController
|
aliceWalletController lnwallet.WalletController
|
||||||
bobWalletController lnwallet.WalletController
|
bobWalletController lnwallet.WalletController
|
||||||
|
|
||||||
|
feeEstimator lnwallet.FeeEstimator
|
||||||
)
|
)
|
||||||
for _, walletDriver := range lnwallet.RegisteredWallets() {
|
|
||||||
tempTestDirAlice, err := ioutil.TempDir("", "lnwallet")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create temp directory: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempTestDirAlice)
|
|
||||||
|
|
||||||
tempTestDirBob, err := ioutil.TempDir("", "lnwallet")
|
tempTestDirAlice, err := ioutil.TempDir("", "lnwallet")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create temp directory: %v", err)
|
t.Fatalf("unable to create temp directory: %v", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tempTestDirBob)
|
defer os.RemoveAll(tempTestDirAlice)
|
||||||
|
|
||||||
walletType := walletDriver.WalletType
|
tempTestDirBob, err := ioutil.TempDir("", "lnwallet")
|
||||||
switch walletType {
|
if err != nil {
|
||||||
case "btcwallet":
|
t.Fatalf("unable to create temp directory: %v", err)
|
||||||
aliceChainRPC, err := chain.NewRPCClient(netParams,
|
}
|
||||||
|
defer os.RemoveAll(tempTestDirBob)
|
||||||
|
|
||||||
|
walletType := walletDriver.WalletType
|
||||||
|
switch walletType {
|
||||||
|
case "btcwallet":
|
||||||
|
var aliceClient, bobClient chain.Interface
|
||||||
|
switch backEnd {
|
||||||
|
case "btcd":
|
||||||
|
feeEstimator, err = lnwallet.NewBtcdFeeEstimator(
|
||||||
|
rpcConfig, 250)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create btcd fee estimator: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
aliceClient, err = chain.NewRPCClient(netParams,
|
||||||
rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
||||||
rpcConfig.Certificates, false, 20)
|
rpcConfig.Certificates, false, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to make chain rpc: %v", err)
|
t.Fatalf("unable to make chain rpc: %v", err)
|
||||||
}
|
}
|
||||||
aliceWalletConfig := &btcwallet.Config{
|
bobClient, err = chain.NewRPCClient(netParams,
|
||||||
PrivatePass: []byte("alice-pass"),
|
|
||||||
HdSeed: aliceHDSeed[:],
|
|
||||||
DataDir: tempTestDirAlice,
|
|
||||||
NetParams: netParams,
|
|
||||||
ChainSource: aliceChainRPC,
|
|
||||||
FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250},
|
|
||||||
}
|
|
||||||
aliceWalletController, err = walletDriver.New(aliceWalletConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create btcwallet: %v", err)
|
|
||||||
}
|
|
||||||
aliceSigner = aliceWalletController.(*btcwallet.BtcWallet)
|
|
||||||
|
|
||||||
bobChainRPC, err := chain.NewRPCClient(netParams,
|
|
||||||
rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
rpcConfig.Host, rpcConfig.User, rpcConfig.Pass,
|
||||||
rpcConfig.Certificates, false, 20)
|
rpcConfig.Certificates, false, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to make chain rpc: %v", err)
|
t.Fatalf("unable to make chain rpc: %v", err)
|
||||||
}
|
}
|
||||||
bobWalletConfig := &btcwallet.Config{
|
case "neutrino":
|
||||||
PrivatePass: []byte("bob-pass"),
|
feeEstimator = lnwallet.StaticFeeEstimator{FeeRate: 250}
|
||||||
HdSeed: bobHDSeed[:],
|
// Set some package-level variable to speed up
|
||||||
DataDir: tempTestDirBob,
|
// operation for tests.
|
||||||
NetParams: netParams,
|
neutrino.WaitForMoreCFHeaders = time.Millisecond * 100
|
||||||
ChainSource: bobChainRPC,
|
neutrino.BanDuration = time.Millisecond * 100
|
||||||
FeeEstimator: lnwallet.StaticFeeEstimator{FeeRate: 250},
|
neutrino.QueryTimeout = time.Millisecond * 500
|
||||||
}
|
neutrino.QueryNumRetries = 2
|
||||||
bobWalletController, err = walletDriver.New(bobWalletConfig)
|
// Start Alice - open a database, start a neutrino
|
||||||
|
// instance, and initialize a btcwallet driver for it.
|
||||||
|
aliceDB, err := walletdb.Create("bdb",
|
||||||
|
tempTestDirAlice+"/neutrino.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create btcwallet: %v", err)
|
t.Fatalf("unable to create DB: %v", err)
|
||||||
|
}
|
||||||
|
defer aliceDB.Close()
|
||||||
|
aliceChain, err := neutrino.NewChainService(
|
||||||
|
neutrino.Config{
|
||||||
|
DataDir: tempTestDirAlice,
|
||||||
|
Database: aliceDB,
|
||||||
|
Namespace: []byte("alice"),
|
||||||
|
ChainParams: *netParams,
|
||||||
|
ConnectPeers: []string{
|
||||||
|
miningNode.P2PAddress(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make neutrino: %v", err)
|
||||||
|
}
|
||||||
|
aliceChain.Start()
|
||||||
|
defer aliceChain.Stop()
|
||||||
|
aliceClient = chain.NewNeutrinoClient(aliceChain)
|
||||||
|
|
||||||
|
// Start Bob - open a database, start a neutrino
|
||||||
|
// instance, and initialize a btcwallet driver for it.
|
||||||
|
bobDB, err := walletdb.Create("bdb",
|
||||||
|
tempTestDirBob+"/neutrino.db")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create DB: %v", err)
|
||||||
|
}
|
||||||
|
defer bobDB.Close()
|
||||||
|
bobChain, err := neutrino.NewChainService(
|
||||||
|
neutrino.Config{
|
||||||
|
DataDir: tempTestDirBob,
|
||||||
|
Database: bobDB,
|
||||||
|
Namespace: []byte("bob"),
|
||||||
|
ChainParams: *netParams,
|
||||||
|
ConnectPeers: []string{
|
||||||
|
miningNode.P2PAddress(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to make neutrino: %v", err)
|
||||||
|
}
|
||||||
|
bobChain.Start()
|
||||||
|
defer bobChain.Stop()
|
||||||
|
bobClient = chain.NewNeutrinoClient(bobChain)
|
||||||
|
case "bitcoind":
|
||||||
|
feeEstimator, err = lnwallet.NewBitcoindFeeEstimator(
|
||||||
|
rpcConfig, 250)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create bitcoind fee estimator: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
// Start a bitcoind instance.
|
||||||
|
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket"
|
||||||
|
defer os.RemoveAll(tempBitcoindDir)
|
||||||
|
rpcPort := rand.Int()%(65536-1024) + 1024
|
||||||
|
bitcoind := exec.Command(
|
||||||
|
"bitcoind",
|
||||||
|
"-datadir="+tempBitcoindDir,
|
||||||
|
"-regtest",
|
||||||
|
"-connect="+miningNode.P2PAddress(),
|
||||||
|
"-txindex",
|
||||||
|
"-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+
|
||||||
|
"d$507c670e800a95284294edb5773b05544b"+
|
||||||
|
"220110063096c221be9933c82d38e1",
|
||||||
|
fmt.Sprintf("-rpcport=%d", rpcPort),
|
||||||
|
"-disablewallet",
|
||||||
|
"-zmqpubrawblock="+zmqPath,
|
||||||
|
"-zmqpubrawtx="+zmqPath,
|
||||||
|
)
|
||||||
|
err = bitcoind.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't start bitcoind: %v", err)
|
||||||
|
}
|
||||||
|
defer bitcoind.Wait()
|
||||||
|
defer bitcoind.Process.Kill()
|
||||||
|
|
||||||
|
// Start an Alice btcwallet bitcoind back end instance.
|
||||||
|
aliceClient, err = chain.NewBitcoindClient(netParams,
|
||||||
|
fmt.Sprintf("127.0.0.1:%d", rpcPort), "weks",
|
||||||
|
"weks", zmqPath, 100*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't start alice client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a Bob btcwallet bitcoind back end instance.
|
||||||
|
bobClient, err = chain.NewBitcoindClient(netParams,
|
||||||
|
fmt.Sprintf("127.0.0.1:%d", rpcPort), "weks",
|
||||||
|
"weks", zmqPath, 100*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't start bob client: %v", err)
|
||||||
}
|
}
|
||||||
bobSigner = bobWalletController.(*btcwallet.BtcWallet)
|
|
||||||
bio = bobWalletController.(*btcwallet.BtcWallet)
|
|
||||||
default:
|
default:
|
||||||
// TODO(roasbeef): add neutrino case
|
t.Fatalf("unknown chain driver: %v", backEnd)
|
||||||
t.Fatalf("unknown wallet driver: %v", walletType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funding via 20 outputs with 4BTC each.
|
aliceWalletConfig := &btcwallet.Config{
|
||||||
alice, err := createTestWallet(tempTestDirAlice, miningNode,
|
PrivatePass: []byte("alice-pass"),
|
||||||
netParams, chainNotifier, aliceWalletController,
|
HdSeed: aliceHDSeed[:],
|
||||||
aliceSigner, bio)
|
DataDir: tempTestDirAlice,
|
||||||
|
NetParams: netParams,
|
||||||
|
ChainSource: aliceClient,
|
||||||
|
FeeEstimator: feeEstimator,
|
||||||
|
}
|
||||||
|
aliceWalletController, err = walletDriver.New(aliceWalletConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test ln wallet: %v", err)
|
t.Fatalf("unable to create btcwallet: %v", err)
|
||||||
}
|
}
|
||||||
defer alice.Shutdown()
|
aliceSigner = aliceWalletController.(*btcwallet.BtcWallet)
|
||||||
|
|
||||||
bob, err := createTestWallet(tempTestDirBob, miningNode,
|
bobWalletConfig := &btcwallet.Config{
|
||||||
netParams, chainNotifier, bobWalletController,
|
PrivatePass: []byte("bob-pass"),
|
||||||
bobSigner, bio)
|
HdSeed: bobHDSeed[:],
|
||||||
|
DataDir: tempTestDirBob,
|
||||||
|
NetParams: netParams,
|
||||||
|
ChainSource: bobClient,
|
||||||
|
FeeEstimator: feeEstimator,
|
||||||
|
}
|
||||||
|
bobWalletController, err = walletDriver.New(bobWalletConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test ln wallet: %v", err)
|
t.Fatalf("unable to create btcwallet: %v", err)
|
||||||
}
|
}
|
||||||
defer bob.Shutdown()
|
bobSigner = bobWalletController.(*btcwallet.BtcWallet)
|
||||||
|
bio = bobWalletController.(*btcwallet.BtcWallet)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown wallet driver: %v", walletType)
|
||||||
|
}
|
||||||
|
|
||||||
// Both wallets should now have 80BTC available for spending.
|
// Funding via 20 outputs with 4BTC each.
|
||||||
assertProperBalance(t, alice, 1, 80)
|
alice, err := createTestWallet(tempTestDirAlice, miningNode, netParams,
|
||||||
assertProperBalance(t, bob, 1, 80)
|
chainNotifier, aliceWalletController, aliceSigner, bio)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create test ln wallet: %v", err)
|
||||||
|
}
|
||||||
|
defer alice.Shutdown()
|
||||||
|
|
||||||
// Execute every test, clearing possibly mutated wallet state
|
bob, err := createTestWallet(tempTestDirBob, miningNode, netParams,
|
||||||
// after each step.
|
chainNotifier, bobWalletController, bobSigner, bio)
|
||||||
for _, walletTest := range walletTests {
|
if err != nil {
|
||||||
testName := fmt.Sprintf("%v:%v", walletType,
|
t.Fatalf("unable to create test ln wallet: %v", err)
|
||||||
walletTest.name)
|
}
|
||||||
success := t.Run(testName, func(t *testing.T) {
|
defer bob.Shutdown()
|
||||||
walletTest.test(miningNode, alice, bob, t)
|
|
||||||
})
|
|
||||||
if !success {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(roasbeef): possible reset mining node's
|
// Both wallets should now have 80BTC available for
|
||||||
// chainstate to initial level, cleanly wipe buckets
|
// spending.
|
||||||
if err := clearWalletStates(alice, bob); err != nil &&
|
assertProperBalance(t, alice, 1, 80)
|
||||||
err != bolt.ErrBucketNotFound {
|
assertProperBalance(t, bob, 1, 80)
|
||||||
t.Fatalf("unable to wipe wallet state: %v", err)
|
|
||||||
}
|
// Execute every test, clearing possibly mutated
|
||||||
|
// wallet state after each step.
|
||||||
|
for _, walletTest := range walletTests {
|
||||||
|
testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd,
|
||||||
|
walletTest.name)
|
||||||
|
success := t.Run(testName, func(t *testing.T) {
|
||||||
|
walletTest.test(miningNode, alice, bob, t)
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(roasbeef): possible reset mining
|
||||||
|
// node's chainstate to initial level, cleanly
|
||||||
|
// wipe buckets
|
||||||
|
if err := clearWalletStates(alice, bob); err !=
|
||||||
|
nil && err != bolt.ErrBucketNotFound {
|
||||||
|
t.Fatalf("unable to wipe wallet state: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
460
routing/chainview/bitcoind.go
Normal file
460
routing/chainview/bitcoind.go
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
package chainview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/roasbeef/btcd/btcjson"
|
||||||
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/roasbeef/btcd/rpcclient"
|
||||||
|
"github.com/roasbeef/btcd/wire"
|
||||||
|
"github.com/roasbeef/btcutil"
|
||||||
|
"github.com/roasbeef/btcwallet/chain"
|
||||||
|
"github.com/roasbeef/btcwallet/wtxmgr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitcoindFilteredChainView is an implementation of the FilteredChainView
|
||||||
|
// interface which is backed by bitcoind.
|
||||||
|
type BitcoindFilteredChainView struct {
|
||||||
|
started int32
|
||||||
|
stopped int32
|
||||||
|
|
||||||
|
// bestHeight is the height of the latest block added to the
|
||||||
|
// blockQueue from the onFilteredConnectedMethod. It is used to
|
||||||
|
// determine up to what height we would need to rescan in case
|
||||||
|
// of a filter update.
|
||||||
|
bestHeightMtx sync.Mutex
|
||||||
|
bestHeight uint32
|
||||||
|
|
||||||
|
// TODO: Factor out common logic between bitcoind and btcd into a
|
||||||
|
// NodeFilteredView interface.
|
||||||
|
chainClient *chain.BitcoindClient
|
||||||
|
|
||||||
|
// blockEventQueue is the ordered queue used to keep the order
|
||||||
|
// of connected and disconnected blocks sent to the reader of the
|
||||||
|
// chainView.
|
||||||
|
blockQueue *blockEventQueue
|
||||||
|
|
||||||
|
// filterUpdates is a channel in which updates to the utxo filter
|
||||||
|
// attached to this instance are sent over.
|
||||||
|
filterUpdates chan filterUpdate
|
||||||
|
|
||||||
|
// chainFilter is the set of utox's that we're currently watching
|
||||||
|
// spends for within the chain.
|
||||||
|
filterMtx sync.RWMutex
|
||||||
|
chainFilter map[wire.OutPoint]struct{}
|
||||||
|
|
||||||
|
// filterBlockReqs is a channel in which requests to filter select
|
||||||
|
// blocks will be sent over.
|
||||||
|
filterBlockReqs chan *filterBlockReq
|
||||||
|
|
||||||
|
quit chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile time check to ensure BitcoindFilteredChainView implements the
|
||||||
|
// chainview.FilteredChainView.
|
||||||
|
var _ FilteredChainView = (*BitcoindFilteredChainView)(nil)
|
||||||
|
|
||||||
|
// NewBitcoindFilteredChainView creates a new instance of a FilteredChainView
|
||||||
|
// from RPC credentials and a ZMQ socket address for a bitcoind instance.
|
||||||
|
func NewBitcoindFilteredChainView(config rpcclient.ConnConfig,
|
||||||
|
zmqConnect string, params chaincfg.Params) (*BitcoindFilteredChainView,
|
||||||
|
error) {
|
||||||
|
chainView := &BitcoindFilteredChainView{
|
||||||
|
chainFilter: make(map[wire.OutPoint]struct{}),
|
||||||
|
filterUpdates: make(chan filterUpdate),
|
||||||
|
filterBlockReqs: make(chan *filterBlockReq),
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
chainConn, err := chain.NewBitcoindClient(¶ms, config.Host,
|
||||||
|
config.User, config.Pass, zmqConnect, 100*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chainView.chainClient = chainConn
|
||||||
|
|
||||||
|
chainView.blockQueue = newBlockEventQueue()
|
||||||
|
|
||||||
|
return chainView, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts all goroutines necessary for normal operation.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) Start() error {
|
||||||
|
// Already started?
|
||||||
|
if atomic.AddInt32(&b.started, 1) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("FilteredChainView starting")
|
||||||
|
|
||||||
|
err := b.chainClient.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, bestHeight, err := b.chainClient.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.bestHeightMtx.Lock()
|
||||||
|
b.bestHeight = uint32(bestHeight)
|
||||||
|
b.bestHeightMtx.Unlock()
|
||||||
|
|
||||||
|
b.blockQueue.Start()
|
||||||
|
|
||||||
|
b.wg.Add(1)
|
||||||
|
go b.chainFilterer()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops all goroutines which we launched by the prior call to the Start
|
||||||
|
// method.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) Stop() error {
|
||||||
|
// Already shutting down?
|
||||||
|
if atomic.AddInt32(&b.stopped, 1) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown the rpc client, this gracefully disconnects from bitcoind's
|
||||||
|
// zmq socket, and cleans up all related resources.
|
||||||
|
b.chainClient.Stop()
|
||||||
|
|
||||||
|
b.blockQueue.Stop()
|
||||||
|
|
||||||
|
log.Infof("FilteredChainView stopping")
|
||||||
|
|
||||||
|
close(b.quit)
|
||||||
|
b.wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// onFilteredBlockConnected is called for each block that's connected to the
|
||||||
|
// end of the main chain. Based on our current chain filter, the block may or
|
||||||
|
// may not include any relevant transactions.
|
||||||
|
func (b *BitcoindFilteredChainView) onFilteredBlockConnected(height int32,
|
||||||
|
hash chainhash.Hash, txns []*wtxmgr.TxRecord) {
|
||||||
|
|
||||||
|
mtxs := make([]*wire.MsgTx, len(txns))
|
||||||
|
for i, tx := range txns {
|
||||||
|
mtxs[i] = &tx.MsgTx
|
||||||
|
|
||||||
|
for _, txIn := range mtxs[i].TxIn {
|
||||||
|
// We can delete this outpoint from the chainFilter, as
|
||||||
|
// we just received a block where it was spent. In case
|
||||||
|
// of a reorg, this outpoint might get "un-spent", but
|
||||||
|
// that's okay since it would never be wise to consider
|
||||||
|
// the channel open again (since a spending transaction
|
||||||
|
// exists on the network).
|
||||||
|
b.filterMtx.Lock()
|
||||||
|
delete(b.chainFilter, txIn.PreviousOutPoint)
|
||||||
|
b.filterMtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// We record the height of the last connected block added to the
|
||||||
|
// blockQueue such that we can scan up to this height in case of
|
||||||
|
// a rescan. It must be protected by a mutex since a filter update
|
||||||
|
// might be trying to read it concurrently.
|
||||||
|
b.bestHeightMtx.Lock()
|
||||||
|
b.bestHeight = uint32(height)
|
||||||
|
b.bestHeightMtx.Unlock()
|
||||||
|
|
||||||
|
block := &FilteredBlock{
|
||||||
|
Hash: hash,
|
||||||
|
Height: uint32(height),
|
||||||
|
Transactions: mtxs,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.blockQueue.Add(&blockEvent{
|
||||||
|
eventType: connected,
|
||||||
|
block: block,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// onFilteredBlockDisconnected is a callback which is executed once a block is
|
||||||
|
// disconnected from the end of the main chain.
|
||||||
|
func (b *BitcoindFilteredChainView) onFilteredBlockDisconnected(height int32,
|
||||||
|
hash chainhash.Hash) {
|
||||||
|
|
||||||
|
log.Debugf("got disconnected block at height %d: %v", height,
|
||||||
|
hash)
|
||||||
|
|
||||||
|
filteredBlock := &FilteredBlock{
|
||||||
|
Hash: hash,
|
||||||
|
Height: uint32(height),
|
||||||
|
}
|
||||||
|
|
||||||
|
b.blockQueue.Add(&blockEvent{
|
||||||
|
eventType: disconnected,
|
||||||
|
block: filteredBlock,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterBlock takes a block hash, and returns a FilteredBlocks which is the
|
||||||
|
// result of applying the current registered UTXO sub-set on the block
|
||||||
|
// corresponding to that block hash. If any watched UTOX's are spent by the
|
||||||
|
// selected lock, then the internal chainFilter will also be updated.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) FilterBlock(blockHash *chainhash.Hash) (*FilteredBlock, error) {
|
||||||
|
req := &filterBlockReq{
|
||||||
|
blockHash: blockHash,
|
||||||
|
resp: make(chan *FilteredBlock, 1),
|
||||||
|
err: make(chan error, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case b.filterBlockReqs <- req:
|
||||||
|
case <-b.quit:
|
||||||
|
return nil, fmt.Errorf("FilteredChainView shutting down")
|
||||||
|
}
|
||||||
|
|
||||||
|
return <-req.resp, <-req.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// chainFilterer is the primary goroutine which: listens for new blocks coming
|
||||||
|
// and dispatches the relevent FilteredBlock notifications, updates the filter
|
||||||
|
// due to requests by callers, and finally is able to preform targeted block
|
||||||
|
// filtration.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): change to use loadfilter RPC's
|
||||||
|
func (b *BitcoindFilteredChainView) chainFilterer() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
|
||||||
|
// filterBlock is a helper funciton that scans the given block, and
|
||||||
|
// notes which transactions spend outputs which are currently being
|
||||||
|
// watched. Additionally, the chain filter will also be updated by
|
||||||
|
// removing any spent outputs.
|
||||||
|
filterBlock := func(blk *wire.MsgBlock) []*wire.MsgTx {
|
||||||
|
var filteredTxns []*wire.MsgTx
|
||||||
|
for _, tx := range blk.Transactions {
|
||||||
|
for _, txIn := range tx.TxIn {
|
||||||
|
prevOp := txIn.PreviousOutPoint
|
||||||
|
if _, ok := b.chainFilter[prevOp]; ok {
|
||||||
|
filteredTxns = append(filteredTxns, tx)
|
||||||
|
|
||||||
|
b.filterMtx.Lock()
|
||||||
|
delete(b.chainFilter, prevOp)
|
||||||
|
b.filterMtx.Unlock()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredTxns
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeJSONBlock := func(block *btcjson.RescannedBlock,
|
||||||
|
height uint32) (*FilteredBlock, error) {
|
||||||
|
hash, err := chainhash.NewHashFromStr(block.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
|
||||||
|
}
|
||||||
|
txs := make([]*wire.MsgTx, 0, len(block.Transactions))
|
||||||
|
for _, str := range block.Transactions {
|
||||||
|
b, err := hex.DecodeString(str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tx := &wire.MsgTx{}
|
||||||
|
err = tx.Deserialize(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
txs = append(txs, tx)
|
||||||
|
}
|
||||||
|
return &FilteredBlock{
|
||||||
|
Hash: *hash,
|
||||||
|
Height: height,
|
||||||
|
Transactions: txs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// The caller has just sent an update to the current chain
|
||||||
|
// filter, so we'll apply the update, possibly rewinding our
|
||||||
|
// state partially.
|
||||||
|
case update := <-b.filterUpdates:
|
||||||
|
|
||||||
|
// First, we'll add all the new UTXO's to the set of
|
||||||
|
// watched UTXO's, eliminating any duplicates in the
|
||||||
|
// process.
|
||||||
|
log.Debugf("Updating chain filter with new UTXO's: %v",
|
||||||
|
update.newUtxos)
|
||||||
|
for _, newOp := range update.newUtxos {
|
||||||
|
b.filterMtx.Lock()
|
||||||
|
b.chainFilter[newOp] = struct{}{}
|
||||||
|
b.filterMtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the new TX filter to the chain client, which
|
||||||
|
// will cause all following notifications from and
|
||||||
|
// calls to it return blocks filtered with the new
|
||||||
|
// filter.
|
||||||
|
b.chainClient.LoadTxFilter(false, []btcutil.Address{},
|
||||||
|
update.newUtxos)
|
||||||
|
|
||||||
|
// All blocks gotten after we loaded the filter will
|
||||||
|
// have the filter applied, but we will need to rescan
|
||||||
|
// the blocks up to the height of the block we last
|
||||||
|
// added to the blockQueue.
|
||||||
|
b.bestHeightMtx.Lock()
|
||||||
|
bestHeight := b.bestHeight
|
||||||
|
b.bestHeightMtx.Unlock()
|
||||||
|
|
||||||
|
// If the update height matches our best known height,
|
||||||
|
// then we don't need to do any rewinding.
|
||||||
|
if update.updateHeight == bestHeight {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll rewind the state to ensure the
|
||||||
|
// caller doesn't miss any relevant notifications.
|
||||||
|
// Starting from the height _after_ the update height,
|
||||||
|
// we'll walk forwards, rescanning one block at a time
|
||||||
|
// with the chain client applying the newly loaded
|
||||||
|
// filter to each block.
|
||||||
|
for i := update.updateHeight + 1; i < bestHeight+1; i++ {
|
||||||
|
blockHash, err := b.chainClient.GetBlockHash(int64(i))
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to get block hash "+
|
||||||
|
"for block at height %d: %v",
|
||||||
|
i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid dealing with the case where a reorg
|
||||||
|
// is happening while we rescan, we scan one
|
||||||
|
// block at a time, skipping blocks that might
|
||||||
|
// have gone missing.
|
||||||
|
rescanned, err := b.chainClient.RescanBlocks(
|
||||||
|
[]chainhash.Hash{*blockHash})
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to rescan block "+
|
||||||
|
"with hash %v at height %d: %v",
|
||||||
|
blockHash, i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no block was returned from the rescan, it
|
||||||
|
// means no matching transactions were found.
|
||||||
|
if len(rescanned) != 1 {
|
||||||
|
log.Tracef("rescan of block %v at "+
|
||||||
|
"height=%d yielded no "+
|
||||||
|
"transactions", blockHash, i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decoded, err := decodeJSONBlock(
|
||||||
|
&rescanned[0], i)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to decode block: %v",
|
||||||
|
err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.blockQueue.Add(&blockEvent{
|
||||||
|
eventType: connected,
|
||||||
|
block: decoded,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've received a new request to manually filter a block.
|
||||||
|
case req := <-b.filterBlockReqs:
|
||||||
|
// First we'll fetch the block itself as well as some
|
||||||
|
// additional information including its height.
|
||||||
|
block, err := b.chainClient.GetBlock(req.blockHash)
|
||||||
|
if err != nil {
|
||||||
|
req.err <- err
|
||||||
|
req.resp <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
header, err := b.chainClient.GetBlockHeaderVerbose(
|
||||||
|
req.blockHash)
|
||||||
|
if err != nil {
|
||||||
|
req.err <- err
|
||||||
|
req.resp <- nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we have this info, we can directly filter the
|
||||||
|
// block and dispatch the proper notification.
|
||||||
|
req.resp <- &FilteredBlock{
|
||||||
|
Hash: *req.blockHash,
|
||||||
|
Height: uint32(header.Height),
|
||||||
|
Transactions: filterBlock(block),
|
||||||
|
}
|
||||||
|
req.err <- err
|
||||||
|
|
||||||
|
// We've received a new event from the chain client.
|
||||||
|
case event := <-b.chainClient.Notifications():
|
||||||
|
switch e := event.(type) {
|
||||||
|
case chain.FilteredBlockConnected:
|
||||||
|
b.onFilteredBlockConnected(e.Block.Height,
|
||||||
|
e.Block.Hash, e.RelevantTxs)
|
||||||
|
case chain.BlockDisconnected:
|
||||||
|
b.onFilteredBlockDisconnected(e.Height, e.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-b.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFilter updates the UTXO filter which is to be consulted when creating
|
||||||
|
// FilteredBlocks to be sent to subscribed clients. This method is cumulative
|
||||||
|
// meaning repeated calls to this method should _expand_ the size of the UTXO
|
||||||
|
// sub-set currently being watched. If the set updateHeight is _lower_ than
|
||||||
|
// the best known height of the implementation, then the state should be
|
||||||
|
// rewound to ensure all relevant notifications are dispatched.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) UpdateFilter(ops []wire.OutPoint, updateHeight uint32) error {
|
||||||
|
select {
|
||||||
|
|
||||||
|
case b.filterUpdates <- filterUpdate{
|
||||||
|
newUtxos: ops,
|
||||||
|
updateHeight: updateHeight,
|
||||||
|
}:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case <-b.quit:
|
||||||
|
return fmt.Errorf("chain filter shutting down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilteredBlocks returns the channel that filtered blocks are to be sent over.
|
||||||
|
// Each time a block is connected to the end of a main chain, and appropriate
|
||||||
|
// FilteredBlock which contains the transactions which mutate our watched UTXO
|
||||||
|
// set is to be returned.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) FilteredBlocks() <-chan *FilteredBlock {
|
||||||
|
return b.blockQueue.newBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectedBlocks returns a receive only channel which will be sent upon
|
||||||
|
// with the empty filtered blocks of blocks which are disconnected from the
|
||||||
|
// main chain in the case of a re-org.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the FilteredChainView interface.
|
||||||
|
func (b *BitcoindFilteredChainView) DisconnectedBlocks() <-chan *FilteredBlock {
|
||||||
|
return b.blockQueue.staleBlocks
|
||||||
|
}
|
@ -4,13 +4,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lightninglabs/neutrino"
|
"github.com/lightninglabs/neutrino"
|
||||||
|
"github.com/ltcsuite/ltcd/btcjson"
|
||||||
"github.com/roasbeef/btcd/btcec"
|
"github.com/roasbeef/btcd/btcec"
|
||||||
"github.com/roasbeef/btcd/chaincfg"
|
"github.com/roasbeef/btcd/chaincfg"
|
||||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||||
@ -25,7 +28,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
netParams = &chaincfg.SimNetParams
|
netParams = &chaincfg.RegressionNetParams
|
||||||
|
|
||||||
testPrivKey = []byte{
|
testPrivKey = []byte{
|
||||||
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
|
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
|
||||||
@ -42,6 +45,39 @@ var (
|
|||||||
testScript, _ = txscript.PayToAddrScript(testAddr)
|
testScript, _ = txscript.PayToAddrScript(testAddr)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func waitForMempoolTx(r *rpctest.Harness, txid *chainhash.Hash) error {
|
||||||
|
var found bool
|
||||||
|
var tx *btcutil.Tx
|
||||||
|
var err error
|
||||||
|
timeout := time.After(10 * time.Second)
|
||||||
|
for !found {
|
||||||
|
// Do a short wait
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
return fmt.Errorf("timeout after 10s")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check for the harness' knowledge of the txid
|
||||||
|
tx, err = r.Node.GetRawTransaction(txid)
|
||||||
|
if err != nil {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *btcjson.RPCError:
|
||||||
|
if e.Code == btcjson.ErrRPCNoTxInfo {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tx != nil && tx.MsgTx().TxHash() == *txid {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getTestTXID(miner *rpctest.Harness) (*chainhash.Hash, error) {
|
func getTestTXID(miner *rpctest.Harness) (*chainhash.Hash, error) {
|
||||||
script, err := txscript.PayToAddrScript(testAddr)
|
script, err := txscript.PayToAddrScript(testAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -131,11 +167,19 @@ func testFilterBlockNotifications(node *rpctest.Harness,
|
|||||||
// private key that we generated above.
|
// private key that we generated above.
|
||||||
txid1, err := getTestTXID(node)
|
txid1, err := getTestTXID(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get test txid")
|
t.Fatalf("unable to get test txid: %v", err)
|
||||||
|
}
|
||||||
|
err = waitForMempoolTx(node, txid1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get test txid in mempool: %v", err)
|
||||||
}
|
}
|
||||||
txid2, err := getTestTXID(node)
|
txid2, err := getTestTXID(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get test txid")
|
t.Fatalf("unable to get test txid: %v", err)
|
||||||
|
}
|
||||||
|
err = waitForMempoolTx(node, txid2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get test txid in mempool: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blockChan := chainView.FilteredBlocks()
|
blockChan := chainView.FilteredBlocks()
|
||||||
@ -218,6 +262,10 @@ func testFilterBlockNotifications(node *rpctest.Harness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to broadcast transaction: %v", err)
|
t.Fatalf("unable to broadcast transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, spendTxid1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get spending txid in mempool: %v", err)
|
||||||
|
}
|
||||||
newBlockHashes, err = node.Node.Generate(1)
|
newBlockHashes, err = node.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
@ -240,6 +288,10 @@ func testFilterBlockNotifications(node *rpctest.Harness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to broadcast transaction: %v", err)
|
t.Fatalf("unable to broadcast transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, spendTxid2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get spending txid in mempool: %v", err)
|
||||||
|
}
|
||||||
newBlockHashes, err = node.Node.Generate(1)
|
newBlockHashes, err = node.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
@ -264,6 +316,10 @@ func testUpdateFilterBackTrack(node *rpctest.Harness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get test txid")
|
t.Fatalf("unable to get test txid")
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get test txid in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Next we'll mine a block confirming the output generated above.
|
// Next we'll mine a block confirming the output generated above.
|
||||||
initBlockHashes, err := node.Node.Generate(1)
|
initBlockHashes, err := node.Node.Generate(1)
|
||||||
@ -306,6 +362,10 @@ func testUpdateFilterBackTrack(node *rpctest.Harness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to broadcast transaction: %v", err)
|
t.Fatalf("unable to broadcast transaction: %v", err)
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, spendTxid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get spending txid in mempool: %v", err)
|
||||||
|
}
|
||||||
newBlockHashes, err := node.Node.Generate(1)
|
newBlockHashes, err := node.Node.Generate(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
@ -352,10 +412,18 @@ func testFilterSingleBlock(node *rpctest.Harness, chainView FilteredChainView,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get test txid")
|
t.Fatalf("unable to get test txid")
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, txid1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get test txid in mempool: %v", err)
|
||||||
|
}
|
||||||
txid2, err := getTestTXID(node)
|
txid2, err := getTestTXID(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get test txid")
|
t.Fatalf("unable to get test txid")
|
||||||
}
|
}
|
||||||
|
err = waitForMempoolTx(node, txid2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get test txid in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
blockChan := chainView.FilteredBlocks()
|
blockChan := chainView.FilteredBlocks()
|
||||||
|
|
||||||
@ -671,7 +739,7 @@ var chainViewTests = []testCase{
|
|||||||
test: testUpdateFilterBackTrack,
|
test: testUpdateFilterBackTrack,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fitler single block",
|
name: "filter single block",
|
||||||
test: testFilterSingleBlock,
|
test: testFilterSingleBlock,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -684,6 +752,68 @@ var interfaceImpls = []struct {
|
|||||||
name string
|
name string
|
||||||
chainViewInit chainViewInitFunc
|
chainViewInit chainViewInitFunc
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "bitcoind_zmq",
|
||||||
|
chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) {
|
||||||
|
// Start a bitcoind instance.
|
||||||
|
tempBitcoindDir, err := ioutil.TempDir("", "bitcoind")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
zmqPath := "ipc:///" + tempBitcoindDir + "/weks.socket"
|
||||||
|
cleanUp1 := func() {
|
||||||
|
os.RemoveAll(tempBitcoindDir)
|
||||||
|
}
|
||||||
|
rpcPort := rand.Int()%(65536-1024) + 1024
|
||||||
|
bitcoind := exec.Command(
|
||||||
|
"bitcoind",
|
||||||
|
"-datadir="+tempBitcoindDir,
|
||||||
|
"-regtest",
|
||||||
|
"-connect="+p2pAddr,
|
||||||
|
"-txindex",
|
||||||
|
"-rpcauth=weks:469e9bb14ab2360f8e226efed5ca6f"+
|
||||||
|
"d$507c670e800a95284294edb5773b05544b"+
|
||||||
|
"220110063096c221be9933c82d38e1",
|
||||||
|
fmt.Sprintf("-rpcport=%d", rpcPort),
|
||||||
|
"-disablewallet",
|
||||||
|
"-zmqpubrawblock="+zmqPath,
|
||||||
|
"-zmqpubrawtx="+zmqPath,
|
||||||
|
)
|
||||||
|
err = bitcoind.Start()
|
||||||
|
if err != nil {
|
||||||
|
cleanUp1()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
cleanUp2 := func() {
|
||||||
|
bitcoind.Process.Kill()
|
||||||
|
bitcoind.Wait()
|
||||||
|
cleanUp1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the bitcoind instance to start up.
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// Start the FilteredChainView implementation instance.
|
||||||
|
config := rpcclient.ConnConfig{
|
||||||
|
Host: fmt.Sprintf(
|
||||||
|
"127.0.0.1:%d", rpcPort),
|
||||||
|
User: "weks",
|
||||||
|
Pass: "weks",
|
||||||
|
DisableAutoReconnect: false,
|
||||||
|
DisableConnectOnNew: true,
|
||||||
|
DisableTLS: true,
|
||||||
|
HTTPPostMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
chainView, err := NewBitcoindFilteredChainView(config,
|
||||||
|
zmqPath, chaincfg.RegressionNetParams)
|
||||||
|
if err != nil {
|
||||||
|
cleanUp2()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return cleanUp2, chainView, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "p2p_neutrino",
|
name: "p2p_neutrino",
|
||||||
chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) {
|
chainViewInit: func(_ rpcclient.ConnConfig, p2pAddr string) (func(), FilteredChainView, error) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user